rubis/apps/api/start/env.ts
ordinarthur 1952265217
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 1m0s
Build & Deploy Landing / build-and-deploy (push) Successful in 31s
Build & Deploy API / build-and-deploy (push) Successful in 1m52s
feat(billing): plans Free/Pro/Business + Stripe Checkout & Customer Portal
Pricing V1 :
  - Free  : 5 factures actives, 1 user, 3 mois de grâce illimité au signup
  - Pro   : 19 €/mois ou 190 €/an, factures illimitées, 1 user
  - Business : 49 €/mois ou 490 €/an, illimité + 5 sièges (V2 multi-users)
              + reply-from-user-email (V2)

Backend :
  - Migration : plan, grace_period_ends_at, stripe_customer_id,
    stripe_subscription_id, subscription_status, billing_cycle,
    current_period_end sur `organizations`. Backfill grace_period auto.
  - `app/services/billing.ts` : PLAN_CAPS, countActiveInvoices,
    canCreateInvoices (enforce post-grace), getOrgSubscriptionState.
  - `app/services/stripe.ts` : client lazy + lookup_keys stables.
  - `app/controllers/billing_controller.ts` :
      • GET  /billing/subscription      → state pour l'UI
      • POST /billing/checkout          → crée une Checkout Session
      • POST /billing/portal            → Customer Portal Session
      • POST /billing/webhook (public)  → handle 4 events Stripe
        (checkout.completed, subscription.updated/deleted, invoice.payment_failed)
  - `commands/stripe_setup.ts` : `node ace stripe:setup` crée Products +
    Prices (idempotent via lookup_key).
  - Enforcement 402 `plan_limit_reached` sur :
      • POST /invoices (saisie manuelle)
      • POST /invoices/import-batch/:id/drafts/:draftId/validate (OCR)

Frontend :
  - `lib/billing.ts` : useSubscription, useStartCheckout, useOpenPortal,
    useIsAtFreeLimit.
  - `routes/_app/parametres_.abonnement.tsx` : page comparaison plans
    avec toggle mensuel/annuel, current plan + portail Stripe, CTA upgrade
    qui redirige vers Checkout hostée.
  - `routes/_app/parametres.tsx` : nouvelle section "Abonnement" qui
    affiche le plan courant + lien vers la page abonnement.
  - `components/billing/PlanLimitBanner.tsx` : banner sur /factures qui
    s'adapte selon période (grâce / approche / atteinte).
  - Toast dédié 402 sur la validation OCR avec action "Passer Pro".

Doc :
  - flow.md : nouvelle section §11 "Pricing & enforcement" qui couvre
    plans, grâce, webhook flow, Customer Portal, env vars.

Setup dev :
  1. STRIPE_SECRET_KEY (sk_test_...) dans apps/api/.env
  2. `stripe listen --forward-to localhost:3333/api/v1/billing/webhook`
     → copier whsec_... → STRIPE_WEBHOOK_SECRET
  3. `node ace stripe:setup` une fois pour créer Products+Prices
  4. Tester via /parametres/abonnement → checkout en mode test Stripe

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 15:03:28 +02:00

95 lines
3.5 KiB
TypeScript

/*
|--------------------------------------------------------------------------
| Environment variables service
|--------------------------------------------------------------------------
|
| The `Env.create` method creates an instance of the Env service. The
| service validates the environment variables and also cast values
| to JavaScript data types.
|
*/
import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), {
// Node
NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const),
PORT: Env.schema.number(),
HOST: Env.schema.string({ format: 'host' }),
LOG_LEVEL: Env.schema.string(),
// App
APP_KEY: Env.schema.secret(),
APP_URL: Env.schema.string({ format: 'url', tld: false }),
// Session
SESSION_DRIVER: Env.schema.enum(['cookie', 'memory', 'database'] as const),
// Database
DB_CONNECTION: Env.schema.enum.optional(['postgres', 'sqlite'] as const),
PG_HOST: Env.schema.string.optional({ format: 'host' }),
PG_PORT: Env.schema.number.optional(),
PG_USER: Env.schema.string.optional(),
PG_PASSWORD: Env.schema.string.optional(),
PG_DB_NAME: Env.schema.string.optional(),
// Redis (BullMQ + cache)
REDIS_HOST: Env.schema.string.optional({ format: 'host' }),
REDIS_PORT: Env.schema.number.optional(),
REDIS_PASSWORD: Env.schema.string.optional(),
// Storage (MinIO via S3 driver)
DRIVE_DISK: Env.schema.enum.optional(['s3', 'fs'] as const),
S3_ENDPOINT: Env.schema.string.optional({ format: 'url', tld: false }),
S3_REGION: Env.schema.string.optional(),
S3_BUCKET: Env.schema.string.optional(),
S3_ACCESS_KEY: Env.schema.string.optional(),
S3_SECRET_KEY: Env.schema.string.optional(),
S3_FORCE_PATH_STYLE: Env.schema.boolean.optional(),
// Mail
MAIL_FROM_ADDRESS: Env.schema.string.optional(),
MAIL_FROM_NAME: Env.schema.string.optional(),
MAIL_DRIVER: Env.schema.enum.optional(['smtp', 'resend'] as const),
SMTP_HOST: Env.schema.string.optional({ format: 'host' }),
SMTP_PORT: Env.schema.number.optional(),
RESEND_API_KEY: Env.schema.string.optional(),
// OCR
OCR_PROVIDER: Env.schema.enum.optional(['mock', 'mistral'] as const),
MISTRAL_API_KEY: Env.schema.string.optional(),
// Stripe — secret key + webhook signing secret. Optional en dev sans
// billing actif. La commande `stripe:setup` et le webhook handler les
// exigent au runtime.
STRIPE_SECRET_KEY: Env.schema.string.optional(),
STRIPE_WEBHOOK_SECRET: Env.schema.string.optional(),
// Web (URL du SPA pour redirects post-checkin)
WEB_URL: Env.schema.string.optional({ format: 'url', tld: false }),
// Auth
ACCESS_TOKEN_TTL_MINUTES: Env.schema.number.optional(),
REFRESH_TOKEN_TTL_DAYS: Env.schema.number.optional(),
COOKIE_DOMAIN: Env.schema.string.optional(),
COOKIE_SECURE: Env.schema.boolean.optional(),
// Google SSO (Ally)
GOOGLE_CLIENT_ID: Env.schema.string.optional(),
GOOGLE_CLIENT_SECRET: Env.schema.string.optional(),
GOOGLE_CALLBACK_URL: Env.schema.string.optional({ format: 'url', tld: false }),
// Microsoft SSO (Ally)
MICROSOFT_CLIENT_ID: Env.schema.string.optional(),
MICROSOFT_CLIENT_SECRET: Env.schema.string.optional(),
MICROSOFT_TENANT: Env.schema.string.optional(),
MICROSOFT_CALLBACK_URL: Env.schema.string.optional({ format: 'url', tld: false }),
/*
|----------------------------------------------------------
| Variables for configuring the limiter package
|----------------------------------------------------------
*/
LIMITER_STORE: Env.schema.enum(['redis', 'memory'] as const)
})