true, // Table names (customizable for multi-tenancy) 'tables' => [ 'cart_items' => 'cart_items', 'carts' => 'carts', 'orders' => 'orders', 'order_notes' => 'order_notes', 'payment_methods' => 'payment_methods', 'payment_provider_identities' => 'payment_provider_identities', 'product_action_runs' => 'product_action_runs', 'product_attributes' => 'product_attributes', 'product_categories' => 'product_categories', 'product_relations' => 'product_relations', 'product_prices' => 'product_prices', 'product_purchases' => 'product_purchases', 'product_actions' => 'product_actions', 'product_stocks' => 'product_stocks', 'product_price_tiers' => 'product_price_tiers', 'products' => 'products', 'cart_discounts' => 'cart_discounts', 'subscriptions' => 'subscriptions', 'subscription_items' => 'subscription_items', 'license_seats' => 'license_seats', 'stripe_transactions' => 'stripe_transactions', ], // Model classes (allow overriding in main instance) 'models' => [ 'product' => \Blax\Shop\Models\Product::class, 'product_price' => \Blax\Shop\Models\ProductPrice::class, 'product_category' => \Blax\Shop\Models\ProductCategory::class, 'product_stock' => \Blax\Shop\Models\ProductStock::class, 'product_price_tier' => \Blax\Shop\Models\ProductPriceTier::class, 'product_attribute' => \Blax\Shop\Models\ProductAttribute::class, 'product_purchase' => \Blax\Shop\Models\ProductPurchase::class, 'cart' => \Blax\Shop\Models\Cart::class, 'cart_item' => \Blax\Shop\Models\CartItem::class, 'order' => \Blax\Shop\Models\Order::class, 'order_note' => \Blax\Shop\Models\OrderNote::class, 'payment_provider_identity' => \Blax\Shop\Models\PaymentProviderIdentity::class, 'payment_method' => \Blax\Shop\Models\PaymentMethod::class, 'subscription' => \Blax\Shop\Models\Subscription::class, 'subscription_item' => \Blax\Shop\Models\SubscriptionItem::class, 'license_seat' => \Blax\Shop\Models\LicenseSeat::class, 'stripe_transaction' => \Blax\Shop\Models\StripeTransaction::class, ], /* * Stripe BalanceTransaction ledger. * * `revenue_types` are the BalanceTransaction `type`s that count toward * "money the business earned, net of fees and refunds". Charges add, refunds * and disputes subtract; per-charge Stripe fees are already inside each row's * `net`. Bank movements (`payout`, `transfer`) are excluded — they move money * that was already counted, they are not revenue or cost. */ 'ledger' => [ /* * Sync the charge's balance transactions into the ledger whenever a * charge, refund or dispute webhook arrives (one Stripe API call each). * Off by default: the ledger then only fills via shop:import-stripe-ledger. */ 'sync_on_webhook' => env('SHOP_LEDGER_SYNC_ON_WEBHOOK', false), 'revenue_types' => [ 'charge', 'payment', 'refund', 'payment_refund', 'adjustment', 'dispute', 'dispute_reversal', // A direct debit (SEPA, ACH) that bounced after it was booked as a // payment: the gross comes back off, plus the failure fee. 'payment_failure_refund', 'refund_failure', // Account-level Stripe fees (Billing, Tax, FX). Stored as fee with // amount 0, so they lower net without counting as refunds. 'stripe_fee', 'stripe_fx_fee', 'tax_fee', // PayPal's own fee on a payment that settled in a PayPal account // (amount 0, net = -fee). See `paypal` below. 'paypal_fee', ], ], /* * PayPal accounts that receive money (read-only, via the Transaction Search * reporting API). `shop:import-paypal-ledger` mirrors each account into the * same ledger as Stripe, with `provider` = paypal and `account` = the PayPal * merchant id, so revenue sums every account the business is paid into. * * PayPal checkouts that run through Stripe settle into the PayPal account: * Stripe's ledger row books the gross and Stripe's fee, the PayPal row adds * PayPal's own fee (`paypal_fee`). Direct PayPal sales (not via Stripe) are * booked in full as `payment`. The REST app needs the "Transaction search" * permission; nothing is ever written to PayPal. * * Add one entry per account; an entry without credentials is skipped. */ 'paypal' => [ 'base_url' => env('PAYPAL_BASE_URL', 'https://api-m.paypal.com'), /* * When true, shop:import-paypal-ledger throws if no account has * credentials or an account's import fails, so a scheduler failure * alert fires instead of PayPal fees silently going uncounted. Off by * default: an app without PayPal stays quiet. */ 'required' => false, 'accounts' => [ [ 'name' => 'default', 'client_id' => env('PAYPAL_CLIENT_ID'), 'secret' => env('PAYPAL_SECRET'), ], ], ], /* * Conditional-price entitlement checker. * * A ProductPrice can carry `meta.requires` (e.g. {"role":"seat"}) so it is * only offered to buyers who already hold something else — a subscription, * role, or prior purchase (loyalty / bundle pricing). The package resolves * the cheapest *eligible* price but delegates "does this buyer satisfy the * requirement" to a host-provided \Blax\Shop\Contracts\EntitlementChecker, * because entitlement semantics are app-specific (laravel-roles, Cashier, * a custom table). Point this at your implementation's class-string. * * Left null, the package binds \Blax\Shop\Services\DenyAllEntitlementChecker * — conditional prices are never offered until a real checker is wired up * (fail-closed: a forgotten binding can't leak a discount to everyone). */ 'entitlement_checker' => null, /* * Subscriptions are Cashier-backed. The package binds its own * Cashier-extending Subscription / SubscriptionItem models (above) so it * can link a subscription to a product and run product actions on the * billing lifecycle. Set `subscriptions.register_cashier_models` to false * if the host app wants to point Cashier at its own models instead. */ 'subscriptions' => [ 'register_cashier_models' => env('SHOP_REGISTER_CASHIER_MODELS', true), // Stripe interval => the product-action event fired on a NEW subscription. 'started_event' => 'subscription.started', 'renewed_event' => 'subscription.renewed', 'canceled_event' => 'subscription.canceled', ], /* * Assignable license seats. * * When a product is seat-based (Product::isSeatBased() — driven by * `meta.seat_based = true`), a purchase/subscription of `quantity = N` * provisions N assignable LicenseSeat rows instead of granting the buyer. * Assigning a seat fires the product's actions for `assigned_event` with an * explicit `grantee`; reclaiming/reassigning fires `revoked_event`. Host * action jobs should honor `grantee` (falling back to the buyer) so the * same jobs serve both personal purchases and seat assignments. */ 'seats' => [ 'enabled' => env('SHOP_SEATS_ENABLED', true), 'assigned_event' => 'seat.assigned', 'revoked_event' => 'seat.revoked', ], /* * Tax rates applied to taxable charges and subscriptions. * * TaxService::rates() is the single source of truth for the applied VAT * rate on every checkout path. Point `rates` at your Stripe tax-rate id(s) * (e.g. 'txr_...' for 19% German VAT) via SHOP_TAX_RATES, either one id or * a comma-separated list. The host app decides tax-exemption (reverse-charge * / zero-rated) and passes it to TaxService::rates($exempt); a repo may also * hand rates() an explicit list instead of reading this config. * * Set SHOP_TAX_REQUIRE=true in production once a rate is configured: a * non-exempt charge with no rate then throws instead of silently billing 0%. */ 'tax' => [ 'rates' => array_values(array_filter( array_map('trim', explode(',', (string) env('SHOP_TAX_RATES', ''))), static fn ($id) => $id !== '', )), 'require' => (bool) env('SHOP_TAX_REQUIRE', false), // Product tax classes whose SALE carries no VAT (TaxService::ratesFor): a // multi-purpose voucher is taxed when redeemed, not when sold (EU VAT // Directive Art. 30b, DE § 3 Abs. 15 UStG). See Product::TAX_CLASS_*. 'untaxed_classes' => ['multi-purpose-voucher'], ], // API Routes configuration 'routes' => [ 'enabled' => true, 'prefix' => 'api/shop', 'middleware' => ['api'], 'name_prefix' => 'shop.', ], // Stock management 'stock' => [ 'track_inventory' => true, 'allow_backorders' => false, 'low_stock_threshold' => 5, 'log_changes' => true, 'auto_release_expired' => true, ], // Product actions (extensible by main instance) 'actions' => [ 'path' => app_path('Jobs/ProductAction'), 'namespace' => 'App\\Jobs\\ProductAction', 'auto_discover' => true, ], // Stripe integration (optional) 'stripe' => [ 'enabled' => env('SHOP_STRIPE_ENABLED', false), 'sync_prices' => true, 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'), // Append the price's name to the Checkout line item ("Flat Art – A4") when a // product is sold in several named prices. Off by default. 'line_item_price_name' => env('SHOP_STRIPE_LINE_ITEM_PRICE_NAME', false), // Webhook events that the shop package listens for // You can customize this list to add/remove events as needed 'webhook_events' => [ // Checkout Session Events 'checkout.session.completed', 'checkout.session.async_payment_succeeded', 'checkout.session.async_payment_failed', 'checkout.session.expired', // Charge Events 'charge.succeeded', 'charge.failed', 'charge.refunded', 'charge.dispute.created', 'charge.dispute.closed', // Payment Intent Events 'payment_intent.succeeded', 'payment_intent.payment_failed', 'payment_intent.canceled', // Refund Events 'refund.created', 'refund.updated', // Invoice Events (for subscriptions) 'invoice.payment_succeeded', 'invoice.payment_failed', ], ], // Currency configuration 'currency' => env('SHOP_CURRENCY', 'usd'), // Cache configuration 'cache' => [ 'enabled' => env('SHOP_CACHE_ENABLED', true), 'ttl' => 3600, 'prefix' => 'shop:', ], // Cart configuration 'cart' => [ // Adding a ProductPrice of a plain product stores the product with that price as the // line (name, photo, checkout, invoice show the product and option). Off by default. 'prices_as_options' => env('SHOP_CART_PRICES_AS_OPTIONS', false), 'expire_after_days' => 30, 'auto_cleanup' => true, 'merge_on_login' => true, // Cart expiration: mark carts as expired after this many minutes of inactivity 'expiration_minutes' => env('SHOP_CART_EXPIRATION_MINUTES', 60), // Cart deletion: delete unused carts after this many hours of inactivity 'deletion_hours' => env('SHOP_CART_DELETION_HOURS', 24), ], // Order configuration 'orders' => [ 'number_prefix' => env('SHOP_ORDER_PREFIX', 'ORD-'), 'auto_complete_virtual' => true, // Auto-complete orders with only virtual products 'auto_complete_paid' => false, // Auto-complete orders when fully paid ], // API Response format 'api' => [ 'include_meta' => true, 'wrap_response' => true, 'response_key' => 'data', ], /* |-------------------------------------------------------------------------- | Loan / rental defaults |-------------------------------------------------------------------------- | | Consumed by ProductPurchase::extend() and ProductPurchase::canExtend() | when no overrides are passed. Lets a host app (library, equipment-rental | etc.) tune the lifecycle without subclassing the model. | */ 'loan' => [ // Loan policy knobs (used by ProductPurchase lifecycle helpers). // Pricing lives on the ProductPrice itself (billing_scheme=tiered + // associated product_price_tiers rows), not here. 'default_duration_weeks' => env('SHOP_LOAN_DURATION_WEEKS', 2), 'extension_weeks' => env('SHOP_LOAN_EXTENSION_WEEKS', 1), 'max_extensions' => env('SHOP_LOAN_MAX_EXTENSIONS', 2), ], /* |-------------------------------------------------------------------------- | API pagination |-------------------------------------------------------------------------- | | Consumed by the public API controllers (Http/Controllers/Api/*). | `per_page` — default page size when the request doesn't specify one. | `max_per_page` — upper bound the controller will honour regardless of | what the client asks for, so a malicious or careless | caller can't request all rows in one request. | */ 'pagination' => [ 'per_page' => env('SHOP_API_PER_PAGE', 24), 'max_per_page' => env('SHOP_API_MAX_PER_PAGE', 100), ], ];