diff --git a/.adonisjs/api.ts b/.adonisjs/api.ts index 7b4f73e..3437c0d 100644 --- a/.adonisjs/api.ts +++ b/.adonisjs/api.ts @@ -150,24 +150,24 @@ type RolesIdDelete = { response: MakeTuyauResponse } type PlansGetHead = { - request: unknown - response: MakeTuyauResponse + request: MakeTuyauRequest> + response: MakeTuyauResponse } type PlansCreateGetHead = { request: unknown response: MakeTuyauResponse } type PlansPost = { - request: unknown - response: MakeTuyauResponse + request: MakeTuyauRequest> + response: MakeTuyauResponse } type PlansIdEditGetHead = { request: unknown response: MakeTuyauResponse } type PlansIdPut = { - request: unknown - response: MakeTuyauResponse + request: MakeTuyauRequest> + response: MakeTuyauResponse } type PlansIdActivatePatch = { request: unknown @@ -181,6 +181,18 @@ type PlansIdDelete = { request: unknown response: MakeTuyauResponse } +type CouponsCreateGetHead = { + request: unknown + response: MakeTuyauResponse +} +type CouponsPost = { + request: MakeTuyauRequest> + response: MakeTuyauResponse +} +type CouponsDelete = { + request: unknown + response: MakeTuyauResponse +} export interface ApiDefinition { 'login': { '$url': { @@ -386,6 +398,18 @@ export interface ApiDefinition { '$delete': PlansIdDelete; }; }; + 'coupons': { + 'create': { + '$url': { + }; + '$get': CouponsCreateGetHead; + '$head': CouponsCreateGetHead; + }; + '$url': { + }; + '$post': CouponsPost; + '$delete': CouponsDelete; + }; } const routes = [ { @@ -717,6 +741,27 @@ const routes = [ method: ["DELETE"], types: {} as PlansIdDelete, }, + { + params: [], + name: 'coupons.create', + path: '/coupons/create', + method: ["GET","HEAD"], + types: {} as CouponsCreateGetHead, + }, + { + params: [], + name: 'coupons.run', + path: '/coupons', + method: ["POST"], + types: {} as CouponsPost, + }, + { + params: [], + name: 'coupons.clear', + path: '/coupons', + method: ["DELETE"], + types: {} as CouponsDelete, + }, ] as const; export const api = { routes, diff --git a/app/actions/coupons/clear_coupons.ts b/app/actions/coupons/clear_coupons.ts new file mode 100644 index 0000000..d94e362 --- /dev/null +++ b/app/actions/coupons/clear_coupons.ts @@ -0,0 +1,25 @@ +import Plans from '#enums/plans' +import Plan from '#models/plan' +import db from '@adonisjs/lucid/services/db' + +export default class ClearCoupons { + static async handle() { + const plans = await Plan.query().whereNot('id', Plans.FREE) + + await db.transaction(async (trx) => { + for (const plan of plans) { + await plan.useTransaction(trx) + + plan.couponCode = null + plan.couponDiscountFixed = null + plan.couponDiscountPercent = null + plan.couponStartAt = null + plan.couponEndAt = null + plan.couponDurationId = null + plan.stripeCouponId = null + + await plan.save() + } + }) + } +} diff --git a/app/actions/coupons/run_coupon.ts b/app/actions/coupons/run_coupon.ts new file mode 100644 index 0000000..4b51918 --- /dev/null +++ b/app/actions/coupons/run_coupon.ts @@ -0,0 +1,19 @@ +import Plan from '#models/plan' +import { couponValidator } from '#validators/coupon' +import db from '@adonisjs/lucid/services/db' +import { Infer } from '@vinejs/vine/types' + +type Params = Infer + +export default class RunCoupon { + static async handle({ planIds, ...data }: Params) { + const plans = await Plan.query().whereIn('id', planIds) + + await db.transaction(async (trx) => { + for (const plan of plans) { + await plan.useTransaction(trx) + await plan.merge(data).save() + } + }) + } +} diff --git a/app/controllers/coupons_controller.ts b/app/controllers/coupons_controller.ts new file mode 100644 index 0000000..85e3a66 --- /dev/null +++ b/app/controllers/coupons_controller.ts @@ -0,0 +1,47 @@ +import ClearCoupons from '#actions/coupons/clear_coupons' +import RunCoupon from '#actions/coupons/run_coupon' +import PlanDto from '#dtos/plan' +import Plans from '#enums/plans' +import Plan from '#models/plan' +import { couponValidator } from '#validators/coupon' +import type { HttpContext } from '@adonisjs/core/http' + +export default class CouponsController { + async create({ inertia, bouncer }: HttpContext) { + await bouncer.with('CmsPolicy').authorize('adminOnly') + + const plans = await Plan.query().whereNot('id', Plans.FREE) + + return inertia.render('coupons/form', { + plans: PlanDto.fromArray(plans), + }) + } + + /** + * Handle form submission for the edit action + */ + async run({ request, response, session, bouncer }: HttpContext) { + await bouncer.with('CmsPolicy').authorize('adminOnly') + + const data = await request.validateUsing(couponValidator) + + await RunCoupon.handle(data) + + session.flash('success', 'Coupon updated successfully') + + return response.redirect().toRoute('plans.index') + } + + /** + * Delete record + */ + async clear({ response, session, bouncer }: HttpContext) { + await bouncer.with('CmsPolicy').authorize('adminOnly') + + await ClearCoupons.handle() + + session.flash('success', 'Coupons cleared successfully') + + return response.redirect().toRoute('plans.index') + } +} diff --git a/app/enums/coupon_durations.ts b/app/enums/coupon_durations.ts index ec65b28..126f443 100644 --- a/app/enums/coupon_durations.ts +++ b/app/enums/coupon_durations.ts @@ -3,5 +3,9 @@ enum CouponDurations { ONCE = 2, } -export default CouponDurations +export const CouponDurationDesc: Record = { + [CouponDurations.FOREVER]: 'Forever', + [CouponDurations.ONCE]: 'Once', +} +export default CouponDurations diff --git a/app/validators/coupon.ts b/app/validators/coupon.ts new file mode 100644 index 0000000..c1d4cfc --- /dev/null +++ b/app/validators/coupon.ts @@ -0,0 +1,25 @@ +import CouponDurations from '#enums/coupon_durations' +import vine from '@vinejs/vine' +import { DateTime } from 'luxon' +import { exists } from './helpers/db.js' + +export const couponValidator = vine.compile( + vine.object({ + couponCode: vine.string().maxLength(100).optional().nullable(), + couponDiscountFixed: vine.number().range([0, 999]).optional().nullable(), + couponDiscountPercent: vine.number().range([0, 999]).optional().nullable(), + couponStartAt: vine + .date() + .optional() + .nullable() + .transform((date) => date && DateTime.fromJSDate(date)), + couponEndAt: vine + .date() + .optional() + .nullable() + .transform((date) => date && DateTime.fromJSDate(date)), + couponDurationId: vine.number().enum(CouponDurations).optional().nullable(), + stripeCouponId: vine.string().optional().nullable(), + planIds: vine.array(vine.number().exists(exists('plans', 'id'))), + }) +) diff --git a/inertia/pages/coupons/form.vue b/inertia/pages/coupons/form.vue new file mode 100644 index 0000000..42be2b0 --- /dev/null +++ b/inertia/pages/coupons/form.vue @@ -0,0 +1,148 @@ + + + diff --git a/inertia/pages/plans/index.vue b/inertia/pages/plans/index.vue index 2fdf7fa..ed291a2 100644 --- a/inertia/pages/plans/index.vue +++ b/inertia/pages/plans/index.vue @@ -1,21 +1,33 @@