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¶
OrderRequest — app/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 status → OrderStatusEnum::OPEN
- Computes subtotal, tax, shipping_charge, shipping_charge_tax, total_amount from current cart items
OrderRetryRequest — app/Http/Requests/Web/Order/OrderRetryRequest.php¶
Applied when retrying a failed payment.
| Field | Rules |
|---|---|
payment_method |
required, Rule::enum(PaymentMethodEnum) |
Cart Validation Rules¶
CartItemRequest — app/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'spurchase_limit. On PATCH for multi-shot CKC variations, also sums other shot-type cart items for the same variation and fails if combined total exceedspurchase_limit.PER_USER,PER_DATE,PER_MEMBER— delegated toPurchaseLimitRuleproviders; fail if the provider'scheckLimit()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.
ProductVariationBuyableRule — app/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¶
LotteryRequest — app/Http/Requests/Web/Lottery/LotteryRequest.php¶
Applied when updating/deleting a lottery application. Uses WithData → LotteryDTO.
| 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 |
LotteryCartRequest — app/Http/Requests/Web/LotteryCart/LotteryCartRequest.php¶
Applied when adding/updating a lottery cart item. Uses WithData → LotteryCartItemDTO.
| 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.
CanApplyVariationRule — app/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.
TotalProductLimit — app/Rules/Lottery/TotalProductLimit.php¶
Prevents the total quantity across all variations of the same product in the lottery cart from exceeding $product->purchase_limit.
Fails with cart.purchase_limit.per_order.
MaxVariationQtyLimit — app/Rules/LotteryCart/MaxVariationQtyLimit.php¶
Prevents quantity for a single variation from exceeding the variation's stock ($variation->quantity).
Fails with lottery.apply_limit.
ApplyLotteryRule — app/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 |