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>
100 lines
2.9 KiB
JSON
100 lines
2.9 KiB
JSON
{
|
|
"name": "@rubis/api",
|
|
"version": "0.1.0",
|
|
"private": true,
|
|
"type": "module",
|
|
"license": "MIT",
|
|
"exports": {
|
|
"./data": "./.adonisjs/client/data.d.ts",
|
|
"./registry": "./.adonisjs/client/registry/index.ts"
|
|
},
|
|
"scripts": {
|
|
"start": "node bin/server.js",
|
|
"build": "node ace build",
|
|
"dev": "node ace serve --hmr",
|
|
"test": "node ace test",
|
|
"lint": "eslint .",
|
|
"format": "prettier --write .",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"imports": {
|
|
"#controllers/*": "./app/controllers/*.js",
|
|
"#exceptions/*": "./app/exceptions/*.js",
|
|
"#models/*": "./app/models/*.js",
|
|
"#mails/*": "./app/mails/*.js",
|
|
"#services/*": "./app/services/*.js",
|
|
"#jobs/*": "./app/jobs/*.js",
|
|
"#listeners/*": "./app/listeners/*.js",
|
|
"#events/*": "./app/events/*.js",
|
|
"#generated/*": "./.adonisjs/server/*.js",
|
|
"#middleware/*": "./app/middleware/*.js",
|
|
"#transformers/*": "./app/transformers/*.js",
|
|
"#validators/*": "./app/validators/*.js",
|
|
"#providers/*": "./providers/*.js",
|
|
"#policies/*": "./app/policies/*.js",
|
|
"#abilities/*": "./app/abilities/*.js",
|
|
"#database/*": "./database/*.js",
|
|
"#tests/*": "./tests/*.js",
|
|
"#start/*": "./start/*.js",
|
|
"#config/*": "./config/*.js"
|
|
},
|
|
"devDependencies": {
|
|
"@adonisjs/assembler": "^8.4.0",
|
|
"@adonisjs/eslint-config": "^3.0.0",
|
|
"@adonisjs/prettier-config": "^1.4.5",
|
|
"@adonisjs/tsconfig": "^2.0.0",
|
|
"@japa/assert": "^4.2.0",
|
|
"@japa/plugin-adonisjs": "^5.2.0",
|
|
"@japa/runner": "^5.3.0",
|
|
"@poppinss/ts-exec": "^1.4.4",
|
|
"@types/luxon": "^3.7.1",
|
|
"@types/node": "~25.6.0",
|
|
"@types/pg": "^8.20.0",
|
|
"@types/react": "^19.2.14",
|
|
"eslint": "^10.2.0",
|
|
"hot-hook": "^1.0.0",
|
|
"pino-pretty": "^13.1.3",
|
|
"prettier": "^3.8.2",
|
|
"tsx": "^4.21.0",
|
|
"typescript": "~6.0.2",
|
|
"youch": "^4.1.1"
|
|
},
|
|
"dependencies": {
|
|
"@adonisjs/ally": "^6.3.0",
|
|
"@adonisjs/auth": "^10.1.0",
|
|
"@adonisjs/bouncer": "^4.0.0",
|
|
"@adonisjs/core": "^7.3.1",
|
|
"@adonisjs/cors": "^3.0.0",
|
|
"@adonisjs/drive": "^4.0.0",
|
|
"@adonisjs/limiter": "^3.0.1",
|
|
"@adonisjs/lucid": "^22.4.2",
|
|
"@adonisjs/mail": "^10.2.0",
|
|
"@adonisjs/session": "^8.1.0",
|
|
"@adonisjs/shield": "^9.0.0",
|
|
"@adonisjs/static": "^2.0.1",
|
|
"@aws-sdk/client-s3": "^3.1043.0",
|
|
"@aws-sdk/s3-request-presigner": "^3.1043.0",
|
|
"@japa/api-client": "^3.2.1",
|
|
"@react-pdf/renderer": "^4.5.1",
|
|
"@tuyau/core": "^1.2.2",
|
|
"@vinejs/vine": "^4.3.1",
|
|
"better-sqlite3": "^12.9.0",
|
|
"bullmq": "^5.76.5",
|
|
"ioredis": "^5.10.1",
|
|
"luxon": "^3.7.2",
|
|
"pg": "^8.20.0",
|
|
"react": "^19.2.5",
|
|
"reflect-metadata": "^0.2.2",
|
|
"stripe": "^22.1.1"
|
|
},
|
|
"hotHook": {
|
|
"boundaries": [
|
|
"./app/controllers/**/*.ts",
|
|
"./app/middleware/*.ts",
|
|
"./app/transformers/**/*.ts",
|
|
"./app/validators/**/*.ts",
|
|
"./app/services/**/*.ts"
|
|
]
|
|
},
|
|
"prettier": "@adonisjs/prettier-config"
|
|
} |