Coupon Integration Points

This document describes how the coupon domain integrates with other modules in the Yoyacoo system.


Order Integration

Snapshot Mechanism

When an order is created with a coupon applied, the order stores:

Column Source Purpose
coupon_id FK → coupons Reference to the applied coupon
coupon_code coupons.code Snapshot of the code used
coupon_discount_type coupons.discount_type Snapshot: fixed_amount / percentage
coupon_discount_value coupons.discount_value Snapshot: discount value at time of use
coupon_discount_amount Calculated Actual yen discounted
settlement_amount final_amount from pricing Final amount after discount

Why Snapshots

Coupons can be edited or deleted after an order is placed. Snapshots on the order preserve the exact discount applied at the time of purchase for:

  • Order history display
  • Refund calculations
  • Accounting / settlement reports
  • Email receipts

nullOnDelete Behavior

The coupon_id FK on orders uses nullOnDelete:

  • If a supplier deletes a coupon, existing orders keep their snapshot columns intact
  • Only the FK reference becomes null
  • Order history remains complete and accurate

Usage Tracking

When an order is successfully completed, a coupon_usages record is created:

  • Links the coupon, supplier, customer email, and order
  • Records the discount_amount and used_at timestamp
  • Enforces uniqueness via (coupon_id, email, order_id) constraint
  • Supports idempotency via optional idempotency_key

Payment Integration

Credit Card Minimum (300 yen)

Per credit card processor requirements, the settlement amount cannot be below 300 yen (except when the total is exactly 0):

credit_settlement_amount = 0                      if final_amount == 0
                         = 300                    if 0 < final_amount < 300
                         = final_amount           if final_amount >= 300

This logic is in CouponPricingService::creditSettlementAmount().

The customer FE uses this value when the selected payment method is CREDIT:

  • settlementAmount = result.credit_settlement_amount
  • Falls back to FE-side getCreditSettlementAmount(finalAmount) if API doesn’t return it

Bank Transfer Coupon Change Warning

When a reserved order (bank transfer payment) has its coupon changed, the system displays a warning about potential payment amount changes. This is handled in the calendar reservation modification flow (spec: 2026-06-12-calendar-bank-transfer-coupon-change-warning-design.md).

Free Order Handling

When final_amount <= 0:

  • The FE resets payment selection (no payment method needed for free orders)
  • settlementAmount is set to 0
  • The order is treated as a free reservation

Email Integration

Supplier Email

File: yoyacoo_be/user/resources/views/emails/html/partials/coupon-price-breakdown.blade.php

When a supplier receives a new reservation email, the email includes a coupon price breakdown partial if a coupon was applied. This shows:

  • Coupon code and discount label
  • Breakdown: subtotal - discount = settlement amount

Customer Email

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

The customer reservation confirmation email includes the same breakdown, showing the customer how much they saved with the coupon.

Both partials are designed as reusable Blade includes that can be embedded in various email templates.


Stripe Integration (Separate Domain)

The codebase has a StripePaymentSubscription trait which contains a method:

public function createCoupon(int $amountOff)

This creates a Stripe Coupon object via the Stripe SDK for subscription-related discounts. This is a separate domain from the business coupon system described in this document. Stripe coupons are used for platform subscription discounts (e.g., free trial periods), not for reservation-level discounts.

Key distinction:

Aspect Business Coupon Stripe Coupon
Purpose Reservation discounts Subscription discounts
Domain App\Domains\Coupon StripePaymentSubscription trait
Database coupons table Stripe API only
Flow Supplier creates → customer applies → order snapshots Platform applies to subscription billing

These two systems do not interact.


User Plan Integration

Fixed Amount Limit

The maximum fixed_amount discount a supplier can create is capped by their plan:

limit = user.user_plan_config.amout_content

If the supplier’s plan doesn’t specify a limit or the value is 0, the fallback is:

limit = config('subscription_setting.subscriptions.free_plan.amout_content', 300000)

This is implemented in CouponController::fixedAmountPlanLimit() and enforced during discount_value validation (max: rule). The percentage discount type always caps at 100 (no plan dependency).


Service Integration

Allowed Service Types

Only specific service types support coupons, configured in coupon_setting.php:

'allowed_service_types' => [1, 2, 12]
Type ID Service Type Description
1 Calendar Date-based booking with menu items
2 Form Form-based reservation with details
12 Event/Seminar Event booking with details

Only paid services (paid_flag = PAID) are eligible for coupons. Free services are excluded from target options.

TargetKind to Service Mapping

TargetKind Service Type Entity DB Table
service Any Entire service services (deprecated for new coupons)
paid_service Any All paid menus/details servicesservice_menus / service_details (price > 0)
service_menu Calendar Specific menu service_menu
service_detail Form / Event Specific detail service_details

Target Validation

When creating/updating specified targets, the backend verifies:

  1. The service belongs to the authenticated supplier
  2. The target entity (menu/detail) exists and belongs to the service
  3. Service-level targets (service, paid_service) must not specify a target_id
  4. Menu/detail targets require a valid target_id

Cross-Module Communication

Shared Package (common)

All coupon models, enums, and the CouponPricingService live in the ReserveApp\Common package, shared across modules:

yoyacoo_be/
├── common/src/
│   ├── Models/
│   │   ├── Coupon.php
│   │   ├── CouponTarget.php
│   │   └── CouponUsage.php
│   └── Enums/Coupon/
│       └── TargetKind.php
├── user/app/Domains/Coupon/
│   └── Controller/CouponController.php    # Supplier CRUD
├── customer/app/Domains/Coupon/
│   ├── Controller/
│   │   ├── CouponValidationController.php  # Customer validate
│   │   └── CouponAvailabilityController.php # Customer availability
│   └── Usecase/
│       └── CouponPricingService.php         # Shared pricing engine
└── admin/  # No coupon-specific controllers (read-only via shared models)

API Communication Flow

Customer FE
    │
    ├── POST /api/customer/coupons/availability
    │   └── CouponAvailabilityController → CouponPricingService::hasActiveCoupon()
    │
    └── POST /api/customer/coupons/validate
        └── CouponValidationController → CouponPricingService::validate()

Supplier FE
    │
    ├── GET    /api/user/coupons           → CouponController@index
    ├── POST   /api/user/coupons           → CouponController@store
    ├── GET    /api/user/coupons/{id}      → CouponController@show
    ├── PUT    /api/user/coupons/{id}      → CouponController@update
    ├── DELETE /api/user/coupons/{id}      → CouponController@destroy
    ├── POST   /api/user/coupons/generate-code → CouponController@generateCode
    └── GET    /api/user/coupons/target-options → CouponController@targetOptions

All APIs use the standard JSON response format with wrap() error handling on the FE side.