-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(coupons): adding coupon run and clear operations
- Loading branch information
Showing
9 changed files
with
355 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof couponValidator> | ||
|
||
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() | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'))), | ||
}) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
<script setup lang="ts"> | ||
import { router, useForm } from '@inertiajs/vue3' | ||
import { Link } from '@tuyau/inertia/vue' | ||
import { Save, Trash2 } from 'lucide-vue-next' | ||
import { tuyau } from '~/lib/tuyau' | ||
import { toast } from 'vue-sonner' | ||
import { DateTime } from 'luxon' | ||
import CouponDurations, { CouponDurationDesc } from '#enums/coupon_durations' | ||
import { enumKeys } from '~/lib/utils' | ||
import PlanDto from '#dtos/plan' | ||
type Form = { | ||
couponCode: string | ||
couponDiscountFixed: number | null | ||
couponDiscountPercent: number | null | ||
couponStartAt: string | ||
couponEndAt: string | ||
couponDurationId: number | ||
stripeCouponId: string | ||
planIds: number[] | ||
} | ||
defineProps<{ plans: PlanDto[] }>() | ||
const form = useForm<Form>({ | ||
couponCode: '', | ||
couponDiscountFixed: null, | ||
couponDiscountPercent: null, | ||
couponStartAt: DateTime.now().toFormat('yyyy-MM-dd'), | ||
couponEndAt: DateTime.now().plus({ weeks: 1 }).toFormat('yyyy-MM-dd'), | ||
couponDurationId: CouponDurations.FOREVER, | ||
stripeCouponId: '', | ||
planIds: [], | ||
}) | ||
function togglePlan(plan: PlanDto) { | ||
if (form.planIds.includes(plan.id)) { | ||
form.planIds = form.planIds.filter((id) => id !== plan.id) | ||
} else { | ||
form.planIds.push(plan.id) | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<div class="flex justify-between items-center gap-3 mb-3"> | ||
<Breadcrumb> | ||
<BreadcrumbList> | ||
<BreadcrumbItem class="hidden md:block"> | ||
<BreadcrumbLink as-child> | ||
<Link route="dashboard"> Dashboard </Link> | ||
</BreadcrumbLink> | ||
</BreadcrumbItem> | ||
<BreadcrumbSeparator class="hidden md:block" /> | ||
<BreadcrumbItem class="hidden md:block"> | ||
<BreadcrumbLink as-child> | ||
<Link route="plans.index"> Plans </Link> | ||
</BreadcrumbLink> | ||
</BreadcrumbItem> | ||
<BreadcrumbSeparator class="hidden md:block" /> | ||
<BreadcrumbItem> | ||
<BreadcrumbPage>Run New Coupon</BreadcrumbPage> | ||
</BreadcrumbItem> | ||
</BreadcrumbList> | ||
</Breadcrumb> | ||
|
||
<div class="flex justify-end items-center gap-1.5"> | ||
<Button @click="form.post(tuyau.$url('coupons.run'))"> | ||
<Save class="w-4 h-4" /> | ||
Run Coupon | ||
</Button> | ||
</div> | ||
</div> | ||
|
||
<div class="flex gap-10"> | ||
<div | ||
class="w-full flex flex-col gap-4 p-3 lg:p-6 bg-white shadow-xl rounded-lg border border-slate-200" | ||
> | ||
<FormInput | ||
label="Coupon Code" | ||
v-model="form.couponCode" | ||
:errors="form.errors.couponCode" | ||
placeholder="Code customer will enter during checkout" | ||
/> | ||
|
||
<FormInput | ||
label="Stripe Coupon ID" | ||
v-model="form.stripeCouponId" | ||
:errors="form.errors.stripeCouponId" | ||
placeholder="Id of the Stripe Coupon" | ||
/> | ||
|
||
<FormInput | ||
label="Fixed Discount Amount" | ||
v-model="form.couponDiscountFixed" | ||
:errors="form.errors.couponDiscountFixed" | ||
type="number" | ||
placeholder="Cent value of fixed discount (ex: 800 = $8.00)" | ||
/> | ||
|
||
<FormInput | ||
label="Percentage Discount Amount" | ||
v-model="form.couponDiscountPercent" | ||
:errors="form.errors.couponDiscountPercent" | ||
type="number" | ||
placeholder="Percent value of percentage discount (ex: 10 = 10%)" | ||
/> | ||
|
||
<FormInput | ||
type="select" | ||
label="Coupon Duration" | ||
v-model="form.couponDurationId" | ||
:errors="form.errors.couponDurationId" | ||
> | ||
<SelectItem | ||
v-for="name in enumKeys(CouponDurations)" | ||
:key="name" | ||
:value="CouponDurations[name]" | ||
> | ||
{{ CouponDurationDesc[CouponDurations[name]] }} | ||
</SelectItem> | ||
</FormInput> | ||
|
||
<div class="flex items-center gap-4"> | ||
<FormInput | ||
type="date" | ||
label="Start Date" | ||
v-model="form.couponStartAt" | ||
:errors="form.errors.couponStartAt" | ||
/> | ||
|
||
<FormInput | ||
type="date" | ||
label="End Date" | ||
v-model="form.couponEndAt" | ||
:errors="form.errors.couponEndAt" | ||
/> | ||
</div> | ||
|
||
<FormInput type="group" label="Plans Included in Sale" :error="form.errors.planIds"> | ||
<Label v-for="plan in plans" :key="plan.id" class="flex items-center gap-2"> | ||
<Checkbox :checked="form.planIds.includes(plan.id)" @update:checked="togglePlan(plan)" /> | ||
<span>{{ plan.name }}</span> | ||
</Label> | ||
</FormInput> | ||
</div> | ||
</div> | ||
</template> |
Oops, something went wrong.