feat: Accept Payments - Stripe Integration with User Tiers (#55) #56
@@ -126,6 +126,9 @@ const secretsSchema = z.object({
|
||||
auth0_management_client_secret: z.string(),
|
||||
google_maps_api_key: z.string(),
|
||||
resend_api_key: z.string(),
|
||||
// Stripe secrets (API keys only - price IDs are config, not secrets)
|
||||
stripe_secret_key: z.string(),
|
||||
stripe_webhook_secret: z.string(),
|
||||
});
|
||||
|
||||
type Config = z.infer<typeof configSchema>;
|
||||
@@ -140,6 +143,10 @@ export interface AppConfiguration {
|
||||
getRedisUrl(): string;
|
||||
getAuth0Config(): { domain: string; audience: string; clientSecret: string };
|
||||
getAuth0ManagementConfig(): { domain: string; clientId: string; clientSecret: string };
|
||||
getStripeConfig(): {
|
||||
secretKey: string;
|
||||
webhookSecret: string;
|
||||
};
|
||||
}
|
||||
|
||||
class ConfigurationLoader {
|
||||
@@ -178,6 +185,8 @@ class ConfigurationLoader {
|
||||
'auth0-management-client-secret',
|
||||
'google-maps-api-key',
|
||||
'resend-api-key',
|
||||
'stripe-secret-key',
|
||||
'stripe-webhook-secret',
|
||||
];
|
||||
|
||||
for (const secretFile of secretFiles) {
|
||||
@@ -240,6 +249,13 @@ class ConfigurationLoader {
|
||||
clientSecret: secrets.auth0_management_client_secret,
|
||||
};
|
||||
},
|
||||
|
||||
getStripeConfig() {
|
||||
return {
|
||||
secretKey: secrets.stripe_secret_key,
|
||||
webhookSecret: secrets.stripe_webhook_secret,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Set RESEND_API_KEY in environment for EmailService
|
||||
|
||||
@@ -45,10 +45,13 @@ Stripe payment integration for subscription tiers and donations.
|
||||
### Webhooks (Public)
|
||||
- POST /api/webhooks/stripe - Stripe webhook (signature verified)
|
||||
|
||||
## Environment Variables
|
||||
## Configuration
|
||||
|
||||
- STRIPE_SECRET_KEY
|
||||
- STRIPE_WEBHOOK_SECRET
|
||||
### Secrets (files via config-loader)
|
||||
- `/run/secrets/stripe-secret-key` - Stripe API secret key
|
||||
- `/run/secrets/stripe-webhook-secret` - Stripe webhook signing secret
|
||||
|
||||
### Environment Variables (docker-compose)
|
||||
- STRIPE_PRO_MONTHLY_PRICE_ID
|
||||
- STRIPE_PRO_YEARLY_PRICE_ID
|
||||
- STRIPE_ENTERPRISE_MONTHLY_PRICE_ID
|
||||
|
||||
@@ -151,11 +151,18 @@ When user downgrades to a tier with fewer vehicle allowance:
|
||||
5. Selections saved to tier_vehicle_selections table
|
||||
6. On upgrade, all vehicles become accessible again
|
||||
|
||||
## Environment Variables
|
||||
## Configuration
|
||||
|
||||
### Secrets (loaded from files via config-loader)
|
||||
Secrets are loaded from `/run/secrets/` (or `SECRETS_DIR` env var):
|
||||
```
|
||||
/run/secrets/stripe-secret-key # Stripe API secret key
|
||||
/run/secrets/stripe-webhook-secret # Stripe webhook signing secret
|
||||
```
|
||||
|
||||
### Environment Variables (docker-compose)
|
||||
Price IDs are not secrets and are configured via environment variables:
|
||||
```
|
||||
STRIPE_SECRET_KEY=sk_live_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
STRIPE_PRO_MONTHLY_PRICE_ID=price_...
|
||||
STRIPE_PRO_YEARLY_PRICE_ID=price_...
|
||||
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID=price_...
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import { logger } from '../../../../core/logging/logger';
|
||||
import { appConfig } from '../../../../core/config/config-loader';
|
||||
import {
|
||||
StripeCustomer,
|
||||
StripeSubscription,
|
||||
@@ -14,18 +15,18 @@ import {
|
||||
|
||||
export class StripeClient {
|
||||
private stripe: Stripe;
|
||||
private webhookSecret: string;
|
||||
|
||||
constructor() {
|
||||
const apiKey = process.env.STRIPE_SECRET_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('STRIPE_SECRET_KEY environment variable is required');
|
||||
}
|
||||
const stripeConfig = appConfig.getStripeConfig();
|
||||
|
||||
this.stripe = new Stripe(apiKey, {
|
||||
this.stripe = new Stripe(stripeConfig.secretKey, {
|
||||
apiVersion: '2025-12-15.clover',
|
||||
typescript: true,
|
||||
});
|
||||
|
||||
this.webhookSecret = stripeConfig.webhookSecret;
|
||||
|
||||
logger.info('Stripe client initialized');
|
||||
}
|
||||
|
||||
@@ -237,15 +238,10 @@ export class StripeClient {
|
||||
*/
|
||||
constructWebhookEvent(payload: Buffer, signature: string): StripeWebhookEvent {
|
||||
try {
|
||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
||||
if (!webhookSecret) {
|
||||
throw new Error('STRIPE_WEBHOOK_SECRET environment variable is required');
|
||||
}
|
||||
|
||||
const event = this.stripe.webhooks.constructEvent(
|
||||
payload,
|
||||
signature,
|
||||
webhookSecret
|
||||
this.webhookSecret
|
||||
);
|
||||
|
||||
logger.info('Stripe webhook event verified', { eventId: event.id, type: event.type });
|
||||
|
||||
Reference in New Issue
Block a user