Skip to content

Validation Flow

Flow Diagram

graph TB
    A["HTTP Request"] --> B["Form Request\nauthorize() + rules()"]
    B -->|invalid| C["422 JSON\nor redirect (CartItemRequest)"]
    B -->|valid| D["Controller"]
    D --> E["WithData trait\nspatie/laravel-data DTO\nSecond validation layer"]
    E --> F["Action / Manager"]
    F --> G["Custom Rule Classes\nBusiness logic validation\ne.g. BuyableRule, PurchaseLimitRule"]
    G -->|fail| H["Validation exception\nredirect or 422"]
    G -->|pass| I["DB operation"]

Purpose

Three-layer validation: HTTP boundary (Form Requests), typed DTO conversion (spatie/laravel-data), and business rule validation (custom Rule classes). Purchase limit enforcement runs at both cart-add and order-placement.

Layer 1: Form Requests

33 Form Request classes across web and admin surfaces.

Path: app/Http/Requests/

Web Requests

Group Requests
Cart CartItemRequest, UpdateCartItemRequest, DeleteCartItemRequest
LotteryCart LotteryCartItemRequest, UpdateLotteryCartItemRequest, DeleteLotteryCartItemRequest
Lottery ApplyLotteryRequest, UpdateLotteryRequest, DeleteLotteryRequest
Order OrderRequest, OrderRetryRequest
Membership Auth requests (login, register, reset, verify), MyPage requests
Product Product view requests
Sales Label requests

Admin Requests

15 Product requests, 4 Order requests, 1 Membership, 1 Sales.

Note: CartItemRequest overrides failedValidation() to redirect back to cart (not 422 JSON) on ProductVariationBuyableRule / BuyableRule failures.


Order Validation Rules

OrderRequestapp/Http/Requests/Web/Order/OrderRequest.php

Applied on order placement (orders.store) and order update.

Field Rules Notes
orderer_name required, string, regex Japanese/ASCII Allows kanji, hiragana, katakana, ASCII, spaces
orderer_name_kana required, string, regex katakana only Full-width katakana (ァ–ヾ) only
orderer_postcode required, string, regex /^[0-9]{7}$/ Exactly 7 digits, no hyphen
orderer_prefecture required, numeric Prefecture code
orderer_city required, string
orderer_address required, string
orderer_tel required, string, regex /^(0{1}\d{9,10})$/ 10–11 digit Japanese phone starting with 0
orderer_birthdate required, closure Checks year/month/date sub-keys all present when value is an array
payment_method required, Rule::enum(PaymentMethodEnum) Only valid enum values accepted

prepareForValidation: On orders.store, merges previously stored session('order_address') into the request so address fields survive the confirmation step.

passedValidation: Enriches the request with computed order fields before it reaches the controller: - Converts orderer_birthdate array to a Carbon instance - Sets statusOrderStatusEnum::OPEN - Computes subtotal, tax, shipping_charge, shipping_charge_tax, total_amount from current cart items

OrderRetryRequestapp/Http/Requests/Web/Order/OrderRetryRequest.php

Applied when retrying a failed payment.

Field Rules
payment_method required, Rule::enum(PaymentMethodEnum)

Cart Validation Rules

CartItemRequestapp/Http/Requests/Web/Cart/CartItemRequest.php

Applies on add-to-cart (POST) and cart update (PATCH). Uses WithData trait — validated data is cast to CartItemDTO.

Field Rules Notes
product_variation_id required, exists:product_variations On POST: also ProductVariationBuyableRule, ProductIfIsLotteryRule
quantity required, numeric, min:1
ckc_shot_type CkcShotTypeRule Only relevant for CKC variations with multiple shot types

Purchase limit inline closure on product_variation_id: Iterates all purchaseLimits on the product and runs CartManager::initializePurchaseLimit() for each:

  • PER_ORDER — checked inline: fails if quantity exceeds the product's purchase_limit. On PATCH for multi-shot CKC variations, also sums other shot-type cart items for the same variation and fails if combined total exceeds purchase_limit.
  • PER_USER, PER_DATE, PER_MEMBER — delegated to PurchaseLimitRule providers; fail if the provider's checkLimit() returns true.

Failure redirect: On POST, if product_variation_id fails via closure, ProductVariationBuyableRule, or ProductIfIsLotteryRule, failedValidation() redirects to orders.cart with errors instead of returning a 422.

ProductVariationBuyableRuleapp/Rules/Cart/ProductVariationBuyableRule.php

Checks buyability at the point of adding to cart (POST only).

Check Condition Error
Product published !$product->is_public "非公開です"
Sale not yet started now() < $product->sale_start_date "販売開始前です"
Sale ended now() > $product->sale_end_date "販売終了しました"
Stock exhausted $variation->quantity === 0 "売り切れました"
CKC event ended CKC product and now() + 10min > $variation->ckc_end_date "販売終了しました"
Variation sale not yet started now() < $variation->sale_start_date "販売開始前です"
Variation sale ended now() > $variation->sale_end_date "販売終了しました"

Lottery Validation Rules

LotteryRequestapp/Http/Requests/Web/Lottery/LotteryRequest.php

Applied when updating/deleting a lottery application. Uses WithDataLotteryDTO.

Field Rules Notes
product_variation_id required, exists:product_variations
quantity numeric, min:0, CanApplyVariationRule
user_id required, exists:users Auto-populated from auth_user()->id in prepareForValidation

LotteryCartRequestapp/Http/Requests/Web/LotteryCart/LotteryCartRequest.php

Applied when adding/updating a lottery cart item. Uses WithDataLotteryCartItemDTO.

Field Rules POST only Notes
product_variation_id required, exists:product_variations, closure Yes Closure checks $product->withinLotteryPeriod(); fails with "応募期間外です。" if outside
quantity required, numeric, min:1, ApplyLotteryRule, TotalProductLimit, MaxVariationQtyLimit No

On PATCH, product_variation_id is resolved from the route's LotteryCartItem model rather than the request body.

CanApplyVariationRuleapp/Rules/Lottery/CanApplyVariationRule.php

Used in LotteryRequest (update flow).

Check Condition Error
Quantity vs stock $value > $variation->quantity lottery.apply_limit
Remaining capacity fetchCanApplyVariationCount(applyCount, applyCountPerDate, applyCountPerMember) < value lottery.apply_limit

applyCount* values are computed by ProductManager and adjusted by subtracting the current application's quantity_applied so editing does not double-count the user's existing application.

TotalProductLimitapp/Rules/Lottery/TotalProductLimit.php

Prevents the total quantity across all variations of the same product in the lottery cart from exceeding $product->purchase_limit.

(quantity being added) + (sum of other variations' qty in lottery cart) <= product.purchase_limit

Fails with cart.purchase_limit.per_order.

MaxVariationQtyLimitapp/Rules/LotteryCart/MaxVariationQtyLimit.php

Prevents quantity for a single variation from exceeding the variation's stock ($variation->quantity).

Fails with lottery.apply_limit.

ApplyLotteryRuleapp/Rules/LotteryCart/ApplyLotteryRule.php

Main lottery cart quantity rule. Runs two checks:

1. Purchase limit providers (via LotteryCartManager::initializeApplyLimit):

Iterates product->purchaseLimits and delegates to the matching provider. All limit types set quantity; providers check against their stored/cart counts.

Provider Limit field checked Logic
PerOrderLimit product.purchase_limit quantity > purchase_limit
PerUserLimit product.purchase_limit_per_user For CKC products: checks current timeslot cart total across variations grouped by (product_id, ckc_start_date). For non-CKC: quantity > purchase_limit_per_user
PerDateLimit product.purchase_limit_per_date getAppliedCount() + getCartItemCount() > purchase_limit_per_date (applied = confirmed lotteries, cart = lottery cart items, both scoped to variation + date)
PerMemberLimit product.purchase_limit_per_member getAppliedCount() + getCartItemCount() > purchase_limit_per_member (scoped to artist member)

2. purchase_limit_per_date inline check (if set):

quantity + (other variations' lottery cart qty for same product) + (confirmed lottery qty for same product+date) <= purchase_limit_per_date

This cross-checks lottery cart items with already-confirmed Lottery records filtered by product_id and ckc_start_date. Fails with lottery.purchase_limit.per_date.

Layer 2: DTO Validation (spatie/laravel-data)

13 DTO classes across modules. 6 Form Requests use the WithData trait — DTOs carry inline spatie validation annotations.

Key DTOs with inline validation: - UserEmailDTO — email format + uniqueness - UserPasswordDTO — password rules - OrderDTO, CartItemDTO, LotteryCartItemDTO, ProductDTO, ProductVariationDTO etc.

Path: app/Modules/*/DTOs/

Layer 3: Custom Rule Classes (21 rules)

Path: app/Rules/

Cart Rules

Rule Validates
ProductVariationBuyableRule Variation is active, in stock, sale dates valid
ProductIfIsLotteryRule Product is in lottery mode when adding to lottery cart
PurchaseLimitRule PER_USER, PER_DATE, PER_MEMBER limits at cart-add
CkcShotTypeRule Valid CKC shot type for variation

Chekicha CSV Rules (6)

Rule Validates
ArtistNameRule CSV artist name matches DB
GroupNameRule CSV group name matches DB
CkcCodeTypeRule Valid CKC code type
CodesAlreadyExistRule No duplicate serial codes in DB
CodesValidFromRule Valid-from date logic
CodesValidUntilRule Valid-until date logic

Lottery Rules

Rule Validates
ApplyLimit Per-lottery application limits via ApplyLimitService providers (PER_ORDER, PER_USER, PER_DATE, PER_MEMBER)
CanApplyVariationRule Quantity ≤ variation stock; remaining capacity via fetchCanApplyVariationCount (subtracts current application to avoid double-counting)
TotalProductLimit All variations' lottery cart qty + new qty ≤ product.purchase_limit

LotteryCart Rules

Rule Validates
ApplyLotteryRule Runs all limit providers (PER_ORDER/PER_USER/PER_DATE/PER_MEMBER) + inline purchase_limit_per_date cross-check vs confirmed Lottery records by product+date
MaxVariationQtyLimit Quantity ≤ variation.quantity (stock cap per variation)

Order Rules

Rule Validates
BuyableRule At order placement: stock recheck (variation.quantity >= qty), then PER_USER/PER_DATE/PER_MEMBER limits (PER_ORDER skipped — enforced at cart add). PER_DATE check includes cart items not yet ordered.

Product Rules

Rule Validates
CkcEndDateRule CKC end date is after start
CompareSaleDateRule Sale start/end date consistency
CompareWithVariationEndDateRule Product date vs variation end date
CompareWithVariationStartDateRule Product date vs variation start date

Global

Rule Validates
Recaptcha Google reCAPTCHA token (registration, inquiry)

Purchase Limit Enforcement

Standard Cart (physical/digital products)

Two checkpoints prevent race conditions:

Cart add (POST/PATCH)
  └─ CartItemRequest inline closure
       ├─ PER_ORDER  → quantity vs product.purchase_limit (+ CKC multi-shot cross-check on PATCH)
       ├─ PER_USER   → PurchaseLimitRule provider
       ├─ PER_DATE   → PurchaseLimitRule provider
       └─ PER_MEMBER → PurchaseLimitRule provider
     Also on POST: ProductVariationBuyableRule (availability + stock)

Order placement (orders.store)
  └─ BuyableRule (per cart item)
       ├─ stock recheck: variation.quantity >= quantity
       ├─ PER_USER   → purchased_count + quantity <= purchase_limit_per_user
       ├─ PER_DATE   → purchased_count + cart_count <= purchase_limit_per_date
       └─ PER_MEMBER → purchased_count + cart_count <= purchase_limit_per_member
       (PER_ORDER is skipped at order placement — already enforced at cart add)

Lottery Cart

Lottery cart add/update (POST/PATCH)
  └─ LotteryCartRequest
       ├─ POST only: withinLotteryPeriod() check on product_variation_id
       └─ quantity rules:
            ├─ ApplyLotteryRule → limit providers (PER_ORDER, PER_USER, PER_DATE, PER_MEMBER)
            │                   + inline PER_DATE cross-check vs confirmed Lottery records
            ├─ TotalProductLimit → total across all variations vs product.purchase_limit
            └─ MaxVariationQtyLimit → quantity vs variation.quantity (stock cap)

Lottery application update (LotteryRequest)
  └─ CanApplyVariationRule
       ├─ quantity vs variation.quantity
       └─ fetchCanApplyVariationCount() respects applied/date/member limits minus current application

Limit types (from PurchaseLimitTypeEnum): PER_ORDER, PER_USER, PER_DATE, PER_MEMBER.

Error Response Formats

Context On failure
Web Form Requests Redirect back with $errors bag (Inertia shared props)
CartItemRequest (buyability rules) Redirect to cart with specific error message
API requests 422 JSON with errors object
DTO validation failure 422 via spatie exception handler
Custom Rule failure Merges into standard validation error bag

Failure Paths

Scenario Behaviour
Cart add exceeds PER_ORDER Inline check in CartItemRequest → redirect to cart
Cart add exceeds PER_USER/DATE/MEMBER PurchaseLimitRule → redirect to cart
Order placement stock exhausted BuyableRule → redirect to cart with error
CKC ZIP import validation fail ChekichaImport returns validation errors to admin UI
Recaptcha failure Registration/inquiry fails with validation error