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_amountandused_attimestamp - 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)
settlementAmountis 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 | services → service_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:
- The service belongs to the authenticated supplier
- The target entity (menu/detail) exists and belongs to the service
- Service-level targets (
service,paid_service) must not specify atarget_id - 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.