Coupon System: Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a full-service coupon system enabling suppliers to create discount coupons and customers to apply them during the reservation flow, with flexible targeting rules, multiple discount types, usage tracking, and order-level snapshot auditing.
Architecture: A domain-driven module shared across supplier and customer portals via a common Laravel package. Supplier-side CRUD backs the /user portal while CouponPricingService in the common layer powers customer validation, pricing, and availability checks. Order-level snapshot columns preserve discount history independently from coupon edits or deletions.
Tech Stack: Laravel 10.48 (PHP 8.2), Next.js 14 (Pages Router), TypeScript, Jotai, Formik + Yup, Tailwind CSS, PHPUnit, Playwright (E2E), OpenAPI 3.0.
Task 1: Database & Infrastructure Setup
Files:
Create:
yoyacoo_be/common/database/migrations/2026_05_11_004207_create_coupons_tables.phpCreate:
yoyacoo_be/common/database/migrations/2026_05_14_000001_add_target_identity_to_coupon_targets_table.phpCreate:
yoyacoo_be/common/database/migrations/2026_05_15_000001_align_coupon_legacy_columns.phpCreate:
yoyacoo_be/common/database/migrations/2026_05_16_000001_add_coupon_fields_to_orders_table.phpCreate:
yoyacoo_be/common/database/migrations/2026_05_26_000001_add_coupon_discount_snapshot_to_orders_table.phpCreate:
yoyacoo_be/common/src/Models/Coupon.phpCreate:
yoyacoo_be/common/src/Models/CouponTarget.phpCreate:
yoyacoo_be/common/src/Models/CouponUsage.phpCreate:
yoyacoo_be/common/src/Enums/Coupon/TargetKind.phpCreate:
yoyacoo_be/user/config/coupon_setting.phpStep 1: Create core coupon tables
Createcoupons(name, code, discount_type/value, date range, target_type, usage_limit_type/count, memo, softDeletes),coupon_targets(coupon_id, service_id, target_kind, target_id), andcoupon_usages(coupon_id, user_id, email, order_id, idempotency_key, discount_amount). Add unique constraints on(user_id, code)and(coupon_id, email, order_id).Step 2: Add target identity migration
Addtarget_kind(string) andtarget_id(unsignedBigInteger) columns tocoupon_targets. Migrate existing data from legacyservice_menu_id/menu_idcolumns into the new format.Step 3: Align legacy column naming
Renamesupplier_idtouser_idon bothcouponsandcoupon_usagestables for consistency with the rest of the system.Step 4: Add coupon columns to orders
Addcoupon_id(FK → coupons, nullable, nullOnDelete),coupon_code(string snapshot),coupon_discount_amount(unsignedInteger, default 0), andsettlement_amount(unsignedInteger, nullable) to theorderstable.Step 5: Add discount snapshot to orders
Addcoupon_discount_type(string, nullable) andcoupon_discount_value(unsignedInteger, nullable) toordersto preserve full discount details for audit trails.Step 6: Create shared models and enums
DefineCouponmodel (SoftDeletes,statusaccessor,formatted_discountaccessor,targets()andusages()relations),CouponTargetmodel (legacy normalization vianormalizedTargetKind()/normalizedTargetId(),matchesTarget()),CouponUsagemodel, andTargetKindPHP enum (SERVICE,PAID_SERVICE,SERVICE_MENU,SERVICE_DETAIL).Step 7: Add coupon config
Createcoupon_setting.phpwithallowed_service_types=[1, 2, 12](calendar, form, event-seminar).Step 8: Run migrations
Runphp artisan migrate.
Task 2: Supplier Backend API (CRUD)
Files:
Create:
yoyacoo_be/user/app/Domains/Coupon/Controller/CouponController.phpModify:
yoyacoo_be/user/routes/api.phpStep 1: Implement coupon listing
GET /api/user/coupons— paginated list scoped to authenticated supplier, with keyword search (name/code) and status filter (active/inactive). Includeusages_countviawithCount. Order bystarts_atdescending.Step 2: Implement coupon creation
POST /api/user/coupons— validate all fields (name, code format regex/^[A-Za-z0-9]+$/, unique code per supplier, discount_value capped by plan limit for fixed_amount, target validation), create coupon, replace targets ifspecifiedtype.Step 3: Implement coupon detail
GET /api/user/coupons/{coupon}— return coupon with loaded targets,usages_count,display_targets(resolved with service/menu names and Japanese labels), andreadable_targets.Step 4: Implement coupon update
PUT /api/user/coupons/{coupon}— partial update with same validation rules as creation, replace targets whentarget_typechanges, delete all targets when switching toall_paid_contents_menus.Step 5: Implement coupon deletion
DELETE /api/user/coupons/{coupon}— soft-delete the coupon. Orders with this coupon preserve their snapshot columns vianullOnDeleteFK.Step 6: Implement code auto-generation
POST /api/user/coupons/generate-code— generate a unique 16-character alphanumeric code (Str::random(16)), retry loop until unique per supplier.Step 7: Implement target options
GET /api/user/coupons/target-options— return eligible targets: all paid services with their menus (calendar) or details (form/event), each as{service_id, target_kind, target_id, label, price}.Step 8: Add supplier routes
Register all 7 coupon endpoints inyoyacoo_be/user/routes/api.php.
Task 3: Customer Backend API (Validation & Pricing)
Files:
Create:
yoyacoo_be/customer/app/Domains/Coupon/Usecase/CouponPricingService.phpCreate:
yoyacoo_be/customer/app/Domains/Coupon/Controller/CouponValidationController.phpCreate:
yoyacoo_be/customer/app/Domains/Coupon/Controller/CouponAvailabilityController.phpModify:
yoyacoo_be/customer/routes/api.phpStep 1: Implement pricing engine core
BuildCouponPricingServicewithvalidate()method:- Look up coupon by
user_id+code, checkstatus === 'active' - Check usage limit per email if
limited_per_email - Build target context from request (
targetContextFromInput) - Match targets via
couponMatchesAnyTarget(supports legacy + new format) - Calculate option subtotals from DB (
ServiceOption+ServiceMenuOption) - Calculate target subtotal from DB prices × quantities
- Calculate eligible subtotal (filtered by matched targets)
- Compute discount:
min(value, eligible)for fixed,floor(eligible × % / 100)for percentage - Compute
final_amount = max(0, billing_subtotal - discount_amount) - Compute
credit_settlement_amountwith 300 yen minimum rule
- Look up coupon by
Step 2: Implement option billing rule
Options are charged in full; discount applies only to menu/detail targets.target_subtotalandoption_subtotalare calculated separately.eligible_subtotalmay include option portion when matched target kind triggers option eligibility (e.g.,SERVICE_DETAILincludes service options).Step 3: Implement availability endpoint
POST /api/customer/coupons/availability— check if supplier has any active coupon (date range, not deleted) whose target matches the request’s service/menu/detail context. Returns{has_active_coupon: boolean}.Step 4: Implement validate endpoint
POST /api/customer/coupons/validate— accept code, supplier_id, service_id, subtotal, email, plus optional menu/detail/option IDs. Validate inputs (service_option/service_menu_option existence viaSoftDeleteExistsrule). CallCouponPricingService::validate(). Return discount breakdown or 400 with Japanese error messages (invalid_or_expired_code,usage_limit_exceeded,coupon_not_applicable).Step 5: Add customer routes
Register both endpoints inyoyacoo_be/customer/routes/api.php.
Task 4: Supplier Frontend
Files:
Create:
yoyacoo_fe/supplier/src/apis/promotion/couponApi.tsCreate:
yoyacoo_fe/supplier/src/pages/promotion/coupons/index.tsxCreate:
yoyacoo_fe/supplier/src/pages/promotion/coupons/create.tsxCreate:
yoyacoo_fe/supplier/src/pages/promotion/coupons/[id]/index.tsxCreate:
yoyacoo_fe/supplier/src/pages/promotion/coupons/[id]/edit.tsxCreate:
yoyacoo_fe/supplier/src/components/organisms/coupons/CouponList.tsxCreate:
yoyacoo_fe/supplier/src/components/organisms/coupons/CouponForm.tsxCreate:
yoyacoo_fe/supplier/src/components/organisms/coupons/CouponDetail.tsxCreate:
yoyacoo_fe/supplier/src/components/organisms/coupons/CouponStatusBadge.tsxCreate:
yoyacoo_fe/supplier/src/components/organisms/coupons/CouponSearchModal.tsxModify:
yoyacoo_fe/supplier/src/utils/const.ts(addNEXT_URLroutes)Modify:
yoyacoo_fe/supplier/src/components/organisms/header/Header.tsxModify:
yoyacoo_fe/supplier/src/components/organisms/sidemenu/index.tsxStep 1: Create API client
Define TypeScript types (Coupon,CouponTarget,TargetOption,CreateCouponRequest,UpdateCouponRequest). Implement methods:indexCoupon,getCoupon,createCoupon,updateCoupon,deleteCoupon,generateCode,getCouponTargets. Include legacy data normalization for target fields.Step 2: Build coupon list page
Table with: page titleクーポン一覧, “create new” button, keyword search input, status filter dropdown (すべて/有効/無効), clear all link, paginated results showing name, code, discount (formatted), target display, date range, usage count, status badge.Step 3: Build coupon form (create/edit)
Formik + Yup form (~700 lines) with: name input, auto-generate code button, discount type toggle (fixed_amount / percentage), discount value with plan limit hint, date range picker (start + optional end), target type selector with target picker (paid_service auto-expands to individual menus/details), usage limit type with count field (1-10), memo textarea. Mobile responsive layout.Step 4: Build coupon detail page
Read-only view with: all coupon fields,CouponStatusBadge(green有効/ gray無効), display targets with resolved names, usage count, edit button, delete button with confirmation modal.Step 5: Add navigation
Add販促header item betweenメール配信and売上. Addクーポン管理sidebar item under promotion section. Apply to both desktop and mobile navigation.Step 6: Mobile optimization
Implement mobile card layout for list, mobile search layout, mobile-responsive create/edit form.
Task 5: Customer Frontend Integration
Files:
Create:
yoyacoo_fe/customer/src/apis/coupon/couponApi.tsCreate:
yoyacoo_fe/customer/src/states/service-form/coupon.tsCreate:
yoyacoo_fe/customer/src/components/organisms/reservations/coupon/CouponCodeField.tsxModify:
yoyacoo_fe/customer/src/states/service-form/types.tsModify:
yoyacoo_fe/customer/src/states/service-form/selectors.tsModify:
yoyacoo_fe/customer/src/states/service-form/pricing.tsModify:
yoyacoo_fe/customer/src/pages/form/[uuid]/index.tsxModify:
yoyacoo_fe/customer/src/pages/form/[uuid]/confirm.tsxModify:
yoyacoo_fe/customer/src/pages/form/[uuid]/payment.tsxCreate:
yoyacoo_fe/swagger/api/customer/components/_coupon.yamlStep 1: Define OpenAPI schemas
AddCouponAvailabilityRequest/ResponseandCouponValidateRequest/Responseschemas to the shared Swagger components.Step 2: Create API client
Generate TypeScript client from OpenAPI spec. ImplementpostCouponAvailabilityandpostCouponValidatecalls.Step 3: Implement state management
Create Jotai-based coupon state:buildCouponAppliedPayment— maps validate response toFormPaymentwith coupon fields (couponId, couponCode, couponDiscountAmount, couponEligibleSubtotal, couponLabel, couponContextKey)buildCouponClearedPayment— resets payment without coupon, recalculates billingAmount and settlementAmountisCouponSupportedService— checks if service type is calendar/form/event-seminarCOUPON_AUTO_REMOVED_MESSAGE— Japanese message when coupon is auto-cleared
Step 4: Build coupon input component
CouponCodeFieldwith two states:- Input state: text input +
適用(Apply) button, calls validate API - Applied state: shows discount label (e.g., “500円OFF”) +
取消(Cancel) button, clears coupon
- Input state: text input +
Step 5: Wire availability check
On reservation form load, call availability API. Ifhas_active_coupon === true, show the coupon input section. Otherwise, hide it entirely.Step 6: Implement auto-removal on selection change
When the user changes menu/detail/option selections, detect the change viacouponContextKeycomparison. If the context changed, auto-clear the applied coupon and showCOUPON_AUTO_REMOVED_MESSAGE.Step 7: Revalidate on confirm/payment
Re-validate the applied coupon at each step transition (form → confirm → payment) to ensure it hasn’t expired or become invalid.
Task 6: Email Integration
Files:
Create:
yoyacoo_be/user/resources/views/emails/html/partials/coupon-price-breakdown.blade.phpCreate:
yoyacoo_be/customer/resources/views/emails/html/partials/coupon-price-breakdown.blade.phpStep 1: Create supplier email partial
Blade partial showing coupon code, discount label, and price breakdown (subtotal - discount = settlement amount). Designed as a reusable include for various supplier email templates.Step 2: Create customer email partial
Mirror the supplier partial for customer reservation confirmation emails, showing the applied discount and final amount.Step 3: Add content label snapshot
Ensure coupon discount labels (formatted as “500円OFF” or “10%OFF”) are captured at order time and displayed consistently in emails regardless of subsequent coupon edits.
Task 7: Testing
Files:
Create:
yoyacoo_be/user/tests/Feature/Domains/Coupon/Controller/CouponControllerTest.phpCreate:
yoyacoo_be/customer/tests/Feature/Domains/Coupon/Controller/CouponValidationTest.phpCreate:
yoyacoo_be/customer/tests/Feature/Domains/Coupon/Controller/CouponAvailabilityTest.phpCreate:
yoyacoo_be/user/tests/Unit/Emails/CouponPriceBreakdownPartialTest.phpCreate:
yoyacoo_be/customer/tests/Unit/Emails/CouponBreakdownMailPlacementTest.phpCreate:
yoyacoo_be/customer/tests/Unit/Common/Src/Models/OrderCouponPriceBreakdownTest.phpCreate:
yoyacoo_be/admin/tests/Unit/CouponModelTest.phpCreate:
yoyacoo_be/admin/tests/Feature/CouponMigrationTest.phpCreate:
yoyacoo_fe/customer/tests/unit/service-form/coupon.test.tsStep 1: Test supplier CRUD
Cover: target options listing, coupon creation with all target kinds, validation errors (percentage > 100, fixed amount exceeds plan limit, code format, usage limit > 10, targets > 20, duplicate code), update targets, detail view with display_targets, usage_count. (~20 tests)Step 2: Test customer coupon validation
Cover: matching targets (menu, detail, paid_service), legacy menu_id, fixed/percentage discount calculation, multiple details with quantities, option pricing, invalid/expired codes, usage limit exceeded, error contract format. (~25 tests)Step 3: Test coupon availability
Cover: active coupon found, no active coupon, supplier with mixed active/inactive coupons.Step 4: Test email partials
Verify price breakdown rendering in both supplier and customer email contexts.Step 5: Test order coupon snapshot
VerifyCouponmodel accessors (status,formatted_discount), order coupon price breakdown with snapshot columns.Step 6: Test admin model and migrations
Unit test for Coupon model behavior, migration test for schema validation.Step 7: Test frontend state management
Unit test for coupon state logic:buildCouponAppliedPayment,buildCouponClearedPayment,isCouponSupportedService.
In Progress
Task 8: Bank Transfer Coupon Change Warning (2026-06-12)
Files:
Create:
yoyacoo_be/common/database/migrations/YYYY_MM_DD_add_coupon_change_warning_to_reservations.phpModify:
yoyacoo_be/user/app/Domains/Reservation/Controller/ReservationController.phpModify:
yoyacoo_fe/supplier/src/components/organisms/reservations/ReservationEditForm.tsxStep 1: Create design spec
Document the warning flow when a reserved order (bank transfer) has its coupon changed.Step 2: Create implementation plan
Break down the implementation into tasks.Step 3: Implement backend warning logic
Detect coupon changes on reservation modification for bank transfer orders.Step 4: Implement frontend warning UI
Display warning dialog about payment amount changes when coupon is modified.
Task 9: Content Label Snapshot (2026-05-26)
Files:
Modify:
yoyacoo_be/common/database/migrations/2026_05_26_000001_add_coupon_discount_snapshot_to_orders_table.phpStep 1: Add snapshot columns to orders
Already completed via migration2026_05_26_000001—coupon_discount_typeandcoupon_discount_valuecolumns.Step 2: Implement content label snapshot
Capture and persist formatted coupon labels at order time for consistent email and UI display.
Backlog / Future
Potential Enhancements
- Coupon analytics dashboard for suppliers (usage stats, revenue impact)
- Bulk coupon creation (e.g., generate multiple codes at once)
- Coupon usage CSV export
- Customer coupon history view
- Coupon A/B testing support
- Time-limited flash coupon support (time-of-day restriction beyond date)
- Minimum order amount condition for coupon applicability
- Stackable coupons (multiple coupons per order)
- Referral coupon tracking
Admin Monitoring
- Admin coupon overview page (usage stats across all suppliers)
- Admin coupon abuse detection
Infrastructure
- Coupon usage analytics pipeline
- Performance optimization for high-traffic coupon validation
- Caching layer for active coupon queries
Project Structure
yoyacoo_be/
├── common/ # Shared package
│ ├── src/Models/Coupon.php
│ ├── src/Models/CouponTarget.php
│ ├── src/Models/CouponUsage.php
│ └── src/Enums/Coupon/TargetKind.php
├── user/ # Supplier module
│ ├── app/Domains/Coupon/Controller/CouponController.php
│ ├── config/coupon_setting.php
│ └── tests/Feature/Domains/Coupon/
└── customer/ # Customer module
├── app/Domains/Coupon/Controller/CouponValidationController.php
├── app/Domains/Coupon/Controller/CouponAvailabilityController.php
├── app/Domains/Coupon/Usecase/CouponPricingService.php
└── tests/Feature/Domains/Coupon/
yoyacoo_fe/
├── supplier/
│ ├── src/apis/promotion/couponApi.ts
│ ├── src/pages/promotion/coupons/
│ └── src/components/organisms/coupons/
├── customer/
│ ├── src/apis/coupon/couponApi.ts
│ ├── src/states/service-form/coupon.ts
│ └── src/components/organisms/reservations/coupon/
└── swagger/api/customer/components/_coupon.yaml
docs/
├── superpowers/specs/coupon/ # Design specs (16 files)
└── superpowers/plans/ # Implementation plans (15 files)