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.php

  • Create: yoyacoo_be/common/database/migrations/2026_05_14_000001_add_target_identity_to_coupon_targets_table.php

  • Create: yoyacoo_be/common/database/migrations/2026_05_15_000001_align_coupon_legacy_columns.php

  • Create: yoyacoo_be/common/database/migrations/2026_05_16_000001_add_coupon_fields_to_orders_table.php

  • Create: yoyacoo_be/common/database/migrations/2026_05_26_000001_add_coupon_discount_snapshot_to_orders_table.php

  • Create: yoyacoo_be/common/src/Models/Coupon.php

  • Create: yoyacoo_be/common/src/Models/CouponTarget.php

  • Create: yoyacoo_be/common/src/Models/CouponUsage.php

  • Create: yoyacoo_be/common/src/Enums/Coupon/TargetKind.php

  • Create: yoyacoo_be/user/config/coupon_setting.php

  • Step 1: Create core coupon tables
    Create coupons (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), and coupon_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
    Add target_kind (string) and target_id (unsignedBigInteger) columns to coupon_targets. Migrate existing data from legacy service_menu_id/menu_id columns into the new format.

  • Step 3: Align legacy column naming
    Rename supplier_id to user_id on both coupons and coupon_usages tables for consistency with the rest of the system.

  • Step 4: Add coupon columns to orders
    Add coupon_id (FK → coupons, nullable, nullOnDelete), coupon_code (string snapshot), coupon_discount_amount (unsignedInteger, default 0), and settlement_amount (unsignedInteger, nullable) to the orders table.

  • Step 5: Add discount snapshot to orders
    Add coupon_discount_type (string, nullable) and coupon_discount_value (unsignedInteger, nullable) to orders to preserve full discount details for audit trails.

  • Step 6: Create shared models and enums
    Define Coupon model (SoftDeletes, status accessor, formatted_discount accessor, targets() and usages() relations), CouponTarget model (legacy normalization via normalizedTargetKind()/normalizedTargetId(), matchesTarget()), CouponUsage model, and TargetKind PHP enum (SERVICE, PAID_SERVICE, SERVICE_MENU, SERVICE_DETAIL).

  • Step 7: Add coupon config
    Create coupon_setting.php with allowed_service_types = [1, 2, 12] (calendar, form, event-seminar).

  • Step 8: Run migrations
    Run php artisan migrate.


Task 2: Supplier Backend API (CRUD)

Files:

  • Create: yoyacoo_be/user/app/Domains/Coupon/Controller/CouponController.php

  • Modify: yoyacoo_be/user/routes/api.php

  • Step 1: Implement coupon listing
    GET /api/user/coupons — paginated list scoped to authenticated supplier, with keyword search (name/code) and status filter (active/inactive). Include usages_count via withCount. Order by starts_at descending.

  • 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 if specified type.

  • 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), and readable_targets.

  • Step 4: Implement coupon update
    PUT /api/user/coupons/{coupon} — partial update with same validation rules as creation, replace targets when target_type changes, delete all targets when switching to all_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 via nullOnDelete FK.

  • 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 in yoyacoo_be/user/routes/api.php.


Task 3: Customer Backend API (Validation & Pricing)

Files:

  • Create: yoyacoo_be/customer/app/Domains/Coupon/Usecase/CouponPricingService.php

  • Create: yoyacoo_be/customer/app/Domains/Coupon/Controller/CouponValidationController.php

  • Create: yoyacoo_be/customer/app/Domains/Coupon/Controller/CouponAvailabilityController.php

  • Modify: yoyacoo_be/customer/routes/api.php

  • Step 1: Implement pricing engine core
    Build CouponPricingService with validate() method:

    • Look up coupon by user_id + code, check status === '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_amount with 300 yen minimum rule
  • Step 2: Implement option billing rule
    Options are charged in full; discount applies only to menu/detail targets. target_subtotal and option_subtotal are calculated separately. eligible_subtotal may include option portion when matched target kind triggers option eligibility (e.g., SERVICE_DETAIL includes 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 via SoftDeleteExists rule). Call CouponPricingService::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 in yoyacoo_be/customer/routes/api.php.


Task 4: Supplier Frontend

Files:

  • Create: yoyacoo_fe/supplier/src/apis/promotion/couponApi.ts

  • Create: yoyacoo_fe/supplier/src/pages/promotion/coupons/index.tsx

  • Create: yoyacoo_fe/supplier/src/pages/promotion/coupons/create.tsx

  • Create: yoyacoo_fe/supplier/src/pages/promotion/coupons/[id]/index.tsx

  • Create: yoyacoo_fe/supplier/src/pages/promotion/coupons/[id]/edit.tsx

  • Create: yoyacoo_fe/supplier/src/components/organisms/coupons/CouponList.tsx

  • Create: yoyacoo_fe/supplier/src/components/organisms/coupons/CouponForm.tsx

  • Create: yoyacoo_fe/supplier/src/components/organisms/coupons/CouponDetail.tsx

  • Create: yoyacoo_fe/supplier/src/components/organisms/coupons/CouponStatusBadge.tsx

  • Create: yoyacoo_fe/supplier/src/components/organisms/coupons/CouponSearchModal.tsx

  • Modify: yoyacoo_fe/supplier/src/utils/const.ts (add NEXT_URL routes)

  • Modify: yoyacoo_fe/supplier/src/components/organisms/header/Header.tsx

  • Modify: yoyacoo_fe/supplier/src/components/organisms/sidemenu/index.tsx

  • Step 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.ts

  • Create: yoyacoo_fe/customer/src/states/service-form/coupon.ts

  • Create: yoyacoo_fe/customer/src/components/organisms/reservations/coupon/CouponCodeField.tsx

  • Modify: yoyacoo_fe/customer/src/states/service-form/types.ts

  • Modify: yoyacoo_fe/customer/src/states/service-form/selectors.ts

  • Modify: yoyacoo_fe/customer/src/states/service-form/pricing.ts

  • Modify: yoyacoo_fe/customer/src/pages/form/[uuid]/index.tsx

  • Modify: yoyacoo_fe/customer/src/pages/form/[uuid]/confirm.tsx

  • Modify: yoyacoo_fe/customer/src/pages/form/[uuid]/payment.tsx

  • Create: yoyacoo_fe/swagger/api/customer/components/_coupon.yaml

  • Step 1: Define OpenAPI schemas
    Add CouponAvailabilityRequest/Response and CouponValidateRequest/Response schemas to the shared Swagger components.

  • Step 2: Create API client
    Generate TypeScript client from OpenAPI spec. Implement postCouponAvailability and postCouponValidate calls.

  • Step 3: Implement state management
    Create Jotai-based coupon state:

    • buildCouponAppliedPayment — maps validate response to FormPayment with coupon fields (couponId, couponCode, couponDiscountAmount, couponEligibleSubtotal, couponLabel, couponContextKey)
    • buildCouponClearedPayment — resets payment without coupon, recalculates billingAmount and settlementAmount
    • isCouponSupportedService — checks if service type is calendar/form/event-seminar
    • COUPON_AUTO_REMOVED_MESSAGE — Japanese message when coupon is auto-cleared
  • Step 4: Build coupon input component
    CouponCodeField with two states:

    • Input state: text input + 適用 (Apply) button, calls validate API
    • Applied state: shows discount label (e.g., “500円OFF”) + 取消 (Cancel) button, clears coupon
  • Step 5: Wire availability check
    On reservation form load, call availability API. If has_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 via couponContextKey comparison. If the context changed, auto-clear the applied coupon and show COUPON_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.php

  • Create: yoyacoo_be/customer/resources/views/emails/html/partials/coupon-price-breakdown.blade.php

  • Step 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.php

  • Create: yoyacoo_be/customer/tests/Feature/Domains/Coupon/Controller/CouponValidationTest.php

  • Create: yoyacoo_be/customer/tests/Feature/Domains/Coupon/Controller/CouponAvailabilityTest.php

  • Create: yoyacoo_be/user/tests/Unit/Emails/CouponPriceBreakdownPartialTest.php

  • Create: yoyacoo_be/customer/tests/Unit/Emails/CouponBreakdownMailPlacementTest.php

  • Create: yoyacoo_be/customer/tests/Unit/Common/Src/Models/OrderCouponPriceBreakdownTest.php

  • Create: yoyacoo_be/admin/tests/Unit/CouponModelTest.php

  • Create: yoyacoo_be/admin/tests/Feature/CouponMigrationTest.php

  • Create: yoyacoo_fe/customer/tests/unit/service-form/coupon.test.ts

  • Step 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
    Verify Coupon model 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.php

  • Modify: yoyacoo_be/user/app/Domains/Reservation/Controller/ReservationController.php

  • Modify: yoyacoo_fe/supplier/src/components/organisms/reservations/ReservationEditForm.tsx

  • Step 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.php

  • Step 1: Add snapshot columns to orders
    Already completed via migration 2026_05_26_000001coupon_discount_type and coupon_discount_value columns.

  • 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)