Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions lib/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,23 +127,38 @@ const start = async () => {
}
};

// Shut down the server
const FORCE_SHUTDOWN_TIMEOUT_MS = 5000;

/**
* Gracefully shut down the server, disconnecting services before exiting.
* If the server promise rejects (e.g. failed to start), logs the error and exits.
* A forced exit is triggered after {@link FORCE_SHUTDOWN_TIMEOUT_MS} ms if shutdown hangs.
*
* @param {Promise<{http: import('http').Server}>} server - Promise returned by {@link start}
* @returns {Promise<void>} Resolves when shutdown is initiated (process exits via callback)
*/
const shutdown = async (server) => {
// Force exit if graceful shutdown hangs
const forceTimeout = setTimeout(() => {
console.error(chalk.red('Forced shutdown (timeout)'));
process.exit(1);
}, FORCE_SHUTDOWN_TIMEOUT_MS);
forceTimeout.unref();

try {
server.then(async (value) => {
await mongooseService.disconnect();
// add sequelize
value.http.close((err) => {
console.info(chalk.yellow('Server closed'));
if (err) {
console.info(chalk.red('Error on server close.', err));
process.exitCode = 1;
}
process.exit();
});
const value = await server;
await mongooseService.disconnect();
value.http.close((err) => {
console.info(chalk.yellow('Server closed'));
if (err) {
console.info(chalk.red('Error on server close.', err));
process.exitCode = 1;
}
process.exit();
});
} catch (e) {
throw new Error(e);
} catch (err) {
console.error(chalk.red('Shutdown error: server never started or shutdown failed'), err);
process.exit(1);
}
};

Expand Down
63 changes: 63 additions & 0 deletions modules/billing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Billing Module

Stripe-based billing with per-plan quota management.

## Quota System

### Configuration

Each downstream project defines its quotas in the billing config:

```js
// config/defaults/development.config.js
billing: {
quotas: {
free: { documents: { create: 10, export: 50 } },
starter: { documents: { create: 100, export: 500 } },
pro: { documents: { create: Infinity, export: Infinity } },
},
}
```

### Middleware — `requireQuota(resource, action)`

Enforces per-plan limits on routes. Returns 429 when quota exceeded.

```js
import requireQuota from '../billing/middlewares/billing.requireQuota.js';

app.route('/api/documents')
.post(passport.authenticate('jwt', { session: false }),
organization.resolveOrganization,
requireQuota('documents', 'create'),
documents.create);
```

### Tracking Usage — `BillingUsageService`

Increment counters after successful operations:

```js
import BillingUsageService from '../billing/services/billing.usage.service.js';

// After creating a document
await BillingUsageService.increment(organizationId, 'documents.create', 1);
```

### Usage Endpoint

`GET /api/billing/usage` — returns current usage, limits, and plan for the authenticated org.

### Events

Listen for plan changes in downstream modules:

```js
import { billingEvents } from '../billing/lib/events.js';

billingEvents.on('plan.changed', ({ organizationId, previousPlan, newPlan, isDowngrade }) => {
if (isDowngrade) {
// Handle downgrade (e.g. disable premium features)
}
});
```
6 changes: 6 additions & 0 deletions modules/billing/config/billing.development.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ const config = {
billing: {
// Plans available for subscriptions — extend as needed
plans: ['free', 'starter', 'pro', 'enterprise'],
// Quotas — downstream projects override these per plan:
// quotas: {
// free: { documents: { create: 10, export: 50 } },
// starter: { documents: { create: 100, export: 500 } },
// pro: { documents: { create: Infinity, export: Infinity } },
// },
// Stripe subscription statuses — see https://docs.stripe.com/api/subscriptions/object#subscription_object-status
statuses: [
'incomplete',
Expand Down
Loading