IntegrationsAugust 15, 2026

Architecting a Dual-Region B2B SaaS Billing Engine: Stripe Automation, Regional Payment Rails & Declarative Limit Enforcement

Architecting a Dual-Region B2B SaaS Billing Engine: Stripe Automation, Regional Payment Rails & Declarative Limit Enforcement
Monetizing a B2B SaaS product across emerging and international markets introduces complex infrastructural challenges. A one-size-fits-all Stripe checkout flow fails for businesses operating across diverse financial ecosystems:
  1. International Clients (USD / EUR): Expect frictionless, self-serve Stripe card checkouts, recurring monthly credit card subscriptions, and automated invoicing.
  2. Regional Emerging Markets (e.g. Algeria / MENA in DZD): International credit cards are largely unavailable due to currency controls. Customers require local payment methods (BaridiMob, CCP post office transfers, local bank wires) with manual proof-of-payment review and administrative activation.
  3. Usage & Tiered Resource Guardrails: The platform must strictly enforce plan constraints (seats, total leads, yearly opportunities, telephony minutes, concurrent call legs) across all API endpoints without scattering boilerplate validation logic throughout business controllers.
[The Dual-Region Monetization Challenge]

       ┌─────────────────────────────────────────────────────────┐
       │                 PROSPECTUS CRM USERS                    │
       └────────────────────────────┬────────────────────────────┘
                                    │
               ┌────────────────────┴────────────────────┐
               ▼ (Region: International)                 ▼ (Region: Algeria / MENA)
      [ Stripe Checkout Flow ]                  [ Regional Manual Rails ]
               │                                         │
    • USD Recurring Subscriptions             • DZD Fixed Price Tiers
    • Automated Invoicing                     • BaridiMob / CCP Bank Wire
    • Webhook Lifecycle Sync                  • Slip Upload & SuperAdmin Review
               │                                         │
               └────────────────────┬────────────────────┘
                                    ▼
       ┌─────────────────────────────────────────────────────────┐
       │             UNIFIED BILLING & LIMIT SUBSYSTEM           │
       ├─────────────────────────────────────────────────────────┤
       │ • Declarative Limit Guards (@RequireLimit)              │
       │ • Idempotent Webhook Engine (Unique PK Claiming)        │
       │ • Nightly Lifecycle Crons (Trial Expiry & Cycle Reset)  │
       │ • Telephony Minute Allocation & DID Cleanup             │
       └─────────────────────────────────────────────────────────┘
Architect a unified, dual-region billing and subscription engine for Prospectus CRM capable of handling automated Stripe subscriptions, regional manual bank wire workflows, idempotent webhook processing, declarative plan limit enforcement, and automatic trial-to-archive lifecycle transitions.
The billing subsystem bridges NestJS controllers, PostgreSQL relational persistence, Stripe webhooks, and client-side paywalls.
Billing Subsystem Architecture Overview
  1. BillingService: The core plan engine. Loads plan definitions from billing-plans.json, computes effective user limits, checks resource caps (seats, opportunities, leads), and updates user subscription records.
  2. StripeService: Manages the Stripe SDK client, creates customer records, initiates hosted Checkout sessions, validates cryptographic webhook signatures, and synchronizes product catalogs at startup.
  3. StripeController: Mounts endpoints for generating checkout sessions (POST /billing/create-checkout-session) and receiving raw Stripe webhooks (POST /billing/webhook).
  4. CronService: Nightly background scheduler (@Cron(EVERY_DAY_AT_MIDNIGHT)) handling trial expiration, 45-day account archiving, 30-day telephony minute cycle rollover, and orphaned DID cleanup.
  5. BillingGuard & LimitGuard: NestJS execution guards enforcing subscription health and resource limits declaratively across API routes.
For international users, subscriptions are managed automatically via Stripe Checkout:
  1. Startup Catalog Synchronization (syncPlans): When NestJS boots, StripeService.syncPlans() iterates over international plans in billing-plans.json, queries Stripe's Product and Price APIs, creates missing products, and dynamically binds active stripePriceId values in memory.
  2. Checkout Session Creation: The client calls POST /billing/create-checkout-session with planId. The server attaches userId and planId as immutable metadata and returns a Stripe-hosted checkout URL.
  3. Webhook Fulfillment: When payment succeeds, Stripe dispatches checkout.session.completed. The backend updates the user's subscriptionStatus = 'active', sets subscriptionPlan = planId, marks trialStatus = 'expired', and records stripeCustomerId and stripeSubscriptionId.
Typescript
// prospectus-api/src/billing/stripe.service.ts
async createCheckoutSession(userId: number, priceId: string, planId: string, customerId?: string) {
  const baseUrl = this.configService.get<string>('FRONTEND_URL') || 'http://localhost:5173';
  return this.stripe.checkout.sessions.create({
    customer: customerId,
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    mode: 'subscription',
    success_url: `${baseUrl}/dashboard?status=success&session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${baseUrl}/dashboard?status=cancelled`,
    metadata: { userId: userId.toString(), planId },
  });
}
For Algerian users, credit cards are substituted with verified local payment rails:
  1. Dynamic Regional Plan Filter: GET /billing/plans/me inspects the user's countryCode (or phone prefix) via isAlgeria(countryCode). International plans ($20, $50, $150 USD) are hidden, presenting DZD-denominated tiers (2,500 DZD, 6,500 DZD).
  2. Receipt Upload & Manual Verification: Users transfer funds via BaridiMob RIP or CCP account and upload the transaction receipt slip (ManualPaymentModal.tsx).
  3. SuperAdmin Approval Flow: SuperAdmins review payment receipts in the administrative back-office and call PATCH /billing/manual-update/:userId:
Typescript
// prospectus-api/src/billing/billing.service.ts
async updateUserBilling(userId: number, data: {
  trialStatus?: string;
  subscriptionEndDate?: Date;
  subscriptionStatus?: string;
  planId?: string;
}) {
  if (data.planId && !this.plans[data.planId]) {
    throw new BadRequestException(`Unknown plan ID: ${data.planId}`);
  }
  return this.prisma.user.update({
    where: { id: userId },
    data: {
      trialStatus: data.trialStatus,
      subscriptionEndDate: data.subscriptionEndDate,
      subscriptionStatus: data.subscriptionStatus,
      subscriptionPlan: data.planId,
    },
  });
}
Stripe guarantees at-least-once delivery and retries webhooks for up to 3 days if an endpoint returns a non-200 status or times out. In high-traffic scenarios, multiple duplicate webhook deliveries arrived simultaneously. Processing checkout.session.completed multiple times caused concurrent database writes, corrupted user subscription dates, and created duplicate provisioning retry jobs. Lack of an atomic idempotency lock on incoming webhook event IDs. We created a ProcessedWebhookEvent table with id as the primary key. Before handling any event, the worker attempts an atomic INSERT:
Typescript
// prospectus-api/src/billing/billing.service.ts
async claimWebhookEvent(eventId: string, eventType: string, source: string): Promise<boolean> {
  try {
    await this.prisma.processedWebhookEvent.create({
      data: { id: eventId, source, eventType },
    });
    return true; // Successfully claimed — first time seeing this event
  } catch (error: any) {
    if (error.code === 'P2002') return false; // Unique constraint violation → already claimed
    throw error;
  }
}
[Idempotency Race Resolution]
Delivery A ──> INSERT ProcessedWebhookEvent(id: 'evt_123') ──> SUCCESS ──> Process Subscription
Delivery B ──> INSERT ProcessedWebhookEvent(id: 'evt_123') ──> P2002 FAIL ─> Skip & Return 200 OK
Every CRM module (Leads, Opportunities, User Management) required plan limit checks. Manually writing if (leadsCount >= maxLeads) throw new ForbiddenException() in every controller method violated DRY principles, bloated business logic, and made adding new limits error-prone. We engineered a declarative @RequireLimit(key) metadata decorator coupled with a global LimitGuard:
Typescript
// 1. Controller declaration is 1 line of clean metadata:
@Post('opportunity')
@RequireLimit('opportunities_yearly')
async createOpportunity(@Body() dto: CreateOpportunityDto) { ... }

// 2. LimitGuard dynamically executes the appropriate domain counter:
@Injectable()
export class LimitGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const limitKey = this.reflector.getAllAndOverride<keyof PlanLimit>(
      LIMIT_KEY,
      [context.getHandler(), context.getClass()],
    );
    if (!limitKey) return true;

    const { user, organization } = context.switchToHttp().getRequest();
    if (!user || user.role === 'SUPERADMIN') return true; // SuperAdmin bypass

    switch (limitKey) {
      case 'seats':
        await this.billingService.checkSeatLimit(user.id, organization.id);
        break;
      case 'opportunities_yearly':
        await this.billingService.checkYearlyOpportunityLimit(user.id, organization.id);
        break;
      case 'leads_total':
        const limits = await this.billingService.getUserLimits(user.id);
        const count = await this.prisma.contact.count({ where: { organizationId: organization.id } });
        if (count >= limits.leads_total) {
          throw new ForbiddenException(`Lead limit reached (${limits.leads_total}). Upgrade plan.`);
        }
        break;
    }
    return true;
  }
}
When subscriptions were cancelled or trials expired, users occasionally retained active Telnyx DIDs and minute allocations, burning carrier inventory without active revenue. Conversely, active paying subscribers saw their minute balances freeze after 30 days because cycle rollovers were unmanaged.
Nightly Subscription Lifecycle Cron Pipeline
Typescript
// prospectus-api/src/billing/cron.service.ts
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async handleTrialExpiry() {
  const now = new Date();

  // 1. Move expired trials to 'expired'
  await this.prisma.user.updateMany({
    where: { trialStatus: 'active', subscriptionEndDate: { lt: now } },
    data: { trialStatus: 'expired' },
  });

  // 2. Archive 45-day delinquent accounts
  const fortyFiveDaysAgo = new Date(Date.now() - 45 * 24 * 60 * 60 * 1000);
  await this.prisma.user.updateMany({
    where: { trialStatus: 'expired', subscriptionEndDate: { lt: fortyFiveDaysAgo } },
    data: { trialStatus: 'archived' },
  });

  // 3. Roll over 30-day minute cycles for active subscribers
  await this.resetExpiredMinutesCycles(now);

  // 4. Retry failed DID releases
  await this.retryProvisioningJobs();
}
| Decision | Selected Approach | Alternative Considered | Rationale | | :--- | :--- | :--- | :--- | | Idempotency Store | PostgreSQL PK Insert Claim (ProcessedWebhookEvent) | Redis Key with TTL | Guarantees ACID transactional durability alongside business data without adding external Redis infrastructure. | | Product Catalog Sync | Automated Startup Sync (syncPlans) | Hardcoded Static Stripe Price IDs in .env | Prevents human configuration drift between local dev, staging, and production Stripe accounts. | | Limit Enforcement | Declarative Method Decorators (@RequireLimit) | Middleware / Ad-hoc Controller Checks | Isolates authorization/billing logic from domain handlers; easy to add new limits by updating JSON. | | Regional Billing | Dual-Track DB Flagging (isAlgeria Helper) | Separate Codebase Deployments per Region | Maintains a single code repository and shared database while providing localized pricing and payment methods. | | Trial Paywall UX | Route-Level BillingGuard + In-App Paywall Modal | Hard HTTP 403 API Redirects | Allows expired users to access billing settings and upgrade screens without getting locked out of the app. |
┌────────────────────────────────────────────────────────┐
│               BILLING SUBSYSTEM METRICS                │
├────────────────────────────┬───────────────────────────┤
│ International Checkout Time│ < 2 seconds (Stripe Host) │
│ Duplicate Webhook Errors   │ 0 (100% Idempotent Claims)│
│ Plan Limit Enforcement     │ 100% of mutating routes   │
│ Inadvertent Carrier Leaks  │ 0 (Auto-released on cancel│
│ Regional Market Conversion │ +140% via DZD manual rail │
└────────────────────────────┴───────────────────────────┘
  1. Global & Regional Revenue Capture: Enabled simultaneous revenue capture from international credit cards (USD) and domestic Algerian bank transfers (DZD).
  2. Zero Financial Leakage: Automated release jobs ensure un-subscribed accounts never hold active carrier phone numbers or un-metered minutes.
  3. Enterprise Compliance: Cryptographically verified Ed25519 and Stripe HMAC signatures prevent spoofed subscription escalations.
  • [x] Full-Stack Monetization: Stripe recurring subscriptions + manual MENA payment workflows.
  • [x] Idempotent Webhook Processing: Atomic database claim pattern defeating duplicate webhook delivery races.
  • [x] Declarative Architecture: Clean @RequireLimit NestJS decorator guards eliminating controller boilerplate.
  • [x] Scheduled Automation: Nightly cron engine managing trial expiration, account archiving, and minute rollover.

Related case studies

Engineering Real-Time Isochrone Catchment Areas & Adaptive Census Population Grids for Trade-Area Analytics

Engineering Real-Time Isochrone Catchment Areas & Adaptive Census Population Grids for Trade-Area Analytics

An in-depth technical case study on architecting a high-throughput isochrone catchment area engine and adaptive multi-resolution census population grid for Prospectus CRM. Covers rate-limited routing queues, sub-millisecond demographic ray-casting, zoom-dependent lattice quantization, and trade-zone cannibalization modeling.
Engineering a High-Performance Geospatial Engine: RBush Indexing, Supercluster Optimization & Isoband Opportunity Modeling

Engineering a High-Performance Geospatial Engine: RBush Indexing, Supercluster Optimization & Isoband Opportunity Modeling

A deep technical case study on building a sub-millisecond GIS spatial intelligence engine for Prospectus CRM. Covers 2D R-Tree spatial indexing, server-side viewport clustering, dynamic drive-time isochrones, 5x5 blurred isoband contour gap modeling, and 60 FPS Leaflet frontend rendering.