惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
大猫的无限游戏
大猫的无限游戏
雷峰网
雷峰网
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
V
Visual Studio Blog
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
美团技术团队
有赞技术团队
有赞技术团队
云风的 BLOG
云风的 BLOG
Google DeepMind News
Google DeepMind News
Blog — PlanetScale
Blog — PlanetScale
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
Vercel News
Vercel News
F
Fortinet All Blogs
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
小众软件
小众软件
月光博客
月光博客
A
About on SuperTechFans

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Stop Hardcoding Your Commercial Rules: Config-Driven Free...
Nasrul Hazim Bin Mohamad · 2026-06-26 · via DEV Community

TL;DR

  • Commercial rules — how many free seats a plan gets, whether you can buy seats in bulk, how tax is applied — drift constantly. Hardcoding them means a deploy every time Sales changes their mind.
  • I moved them behind a BillingProvider contract plus config/settings: the number is data, the behaviour is a swappable driver (manual vs Stripe).
  • Takeaway: if a value is a business decision, not an engineering one, it belongs in config or settings — not an if statement.

A "free tier of 3 seats" looks innocent until someone asks for 5. Then you grep the codebase, find the 3 in four places, miss one, and ship a billing bug. Today's work was about making sure that number — and a few friends — never live in code again.

The seam: one contract, two drivers

Billing has two modes: invoice-by-hand (early/enterprise customers) and Stripe (self-serve). Callers shouldn't care which. So everything goes through a contract:

interface BillingProvider
{
    public function freeSeatDefault(): int;
    public function purchaseSeats(Account $account, int $quantity): SeatPurchase;
    public function appliesTax(): bool;
    // ...
}

ManualBillingProvider and StripeBillingProvider implement it. The entitlement gate that asks "does this account have a seat available?" talks to the contract, never to Stripe directly. Same idea as a Filesystem disk — the caller says "store this", the driver decides where.

The free-seat default is config, not a constant

// config/billing.php
'seats' => [
    'free_default' => env('BILLING_FREE_SEATS', 1),
    'bulk_enabled' => true,
],

The manual provider reads the config; the Stripe provider can override per-plan from the subscription metadata. Customer-facing tweaks (a promo bumping free seats to 5) move to DB-backed settings so an admin flips it without a deploy. Code reads one path; the value's origin is an implementation detail.

Bulk seats: quantity is a first-class input

Adding seats one-by-one is fine until a customer onboards 40 people. Bulk just means purchaseSeats($account, 40) instead of a loop — the provider decides how to price and record it. The win is that the gate logic doesn't change: it still asks "seats_used < seats_total?".

Tax belongs to the provider, not the checkout view

Tax rules vary by region and change without warning. Putting tax math in a Blade view or controller is how you end up auditing checkout code. Instead, tax is a provider concern — the Stripe driver delegates to Stripe Tax (it already knows the customer's location and current rates), the manual driver returns "tax handled on the invoice". The app just asks appliesTax().

checkout ──► BillingProvider (contract)
                 ├─ ManualBillingProvider ─► invoice handles tax
                 └─ StripeBillingProvider ─► Stripe Tax computes it

Test the rule, not the vendor

You don't need a live Stripe call to prove the policy. Bind a fake provider and assert behaviour:

it('grants the configured number of free seats', function () {
    config(['billing.seats.free_default' => 3]);

    $account = Account::factory()->create();

    expect(app(BillingProvider::class)->freeSeatDefault())->toBe(3);
    expect($account->availableSeats())->toBe(3);
});

The contract is the seam and the test boundary — swap in a manual provider, assert the entitlement gate counts seats correctly, done. No network, no flake.

Decision rule

If the value… Put it in…
Never changes, is engineering-internal a constant
Changes per environment/deploy config() + .env
A customer or admin should change live DB-backed settings
Is a swappable behaviour a driver behind a contract

Free seats, bulk pricing, tax handling — every one of them is a business decision dressed up as a code value. Move them out, and the next "can we make it 5?" is a settings toggle, not a release.