Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .claude/skills/feature/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ If an action affects another user:
- [ ] Actions affecting other users trigger email (if configured)
- [ ] Templates created for each email type

**Error documentation:**
- [ ] If a non-obvious bug was fixed, add a single-line entry to `ERRORS.md` (root of repo) using format: `[YYYY-MM-DD] <scope>: <wrong> -> <right>` (see existing examples in `ERRORS.md`)

### 8b. Elegance check

For non-trivial changes: pause and ask yourself "is there a simpler or more elegant approach?" If the current implementation feels hacky, refactor before proceeding.
Expand Down
2 changes: 1 addition & 1 deletion modules/billing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Increment counters after successful operations:
import BillingUsageService from '../billing/services/billing.usage.service.js';

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

### Usage Endpoint
Expand Down
4 changes: 2 additions & 2 deletions modules/billing/controllers/billing.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const getUsage = async (req, res) => {
// Get usage counters (includes month field)
const usage = await BillingUsageService.get(req.organization._id.toString());

// Flatten quotas config into { "resource.action": limit } format
// Flatten quotas config into { "resource_action": limit } format
// Normalize Infinity to null for JSON-safe serialization
const quotas = config.billing?.quotas;
let limits = {};
Expand All @@ -82,7 +82,7 @@ const getUsage = async (req, res) => {
for (const resource of Object.keys(planQuotas)) {
for (const action of Object.keys(planQuotas[resource])) {
const rawLimit = planQuotas[resource][action];
limits[`${resource}.${action}`] = Number.isFinite(rawLimit) ? rawLimit : null;
limits[`${resource}_${action}`] = Number.isFinite(rawLimit) ? rawLimit : null;
}
Comment thread
PierreBrisorgueil marked this conversation as resolved.
}
}
Expand Down
2 changes: 1 addition & 1 deletion modules/billing/middlewares/billing.requireQuota.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function requireQuota(resource, action) {

// Check current usage
const usage = await BillingUsageService.get(req.organization._id.toString());
const counterKey = `${resource}.${action}`;
const counterKey = `${resource}_${action}`;
const current = usage.counters[counterKey] || 0;

if (current >= limit) {
Expand Down
14 changes: 7 additions & 7 deletions modules/billing/tests/billing.quota.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('requireQuota middleware:', () => {

test('should allow request when under quota', async () => {
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'free', status: 'active' });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 1 } });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps_create': 1 } });

await requireQuota('scraps', 'create')(req, res, next);

Expand All @@ -79,7 +79,7 @@ describe('requireQuota middleware:', () => {

test('should return 429 when at quota limit', async () => {
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'free', status: 'active' });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 3 } });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps_create': 3 } });

await requireQuota('scraps', 'create')(req, res, next);

Expand All @@ -95,7 +95,7 @@ describe('requireQuota middleware:', () => {

test('should return 429 when over quota limit', async () => {
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'free', status: 'active' });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 5 } });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps_create': 5 } });

await requireQuota('scraps', 'create')(req, res, next);

Expand All @@ -105,7 +105,7 @@ describe('requireQuota middleware:', () => {

test('should treat missing subscription as free plan', async () => {
mockSubscriptionRepository.findByOrganization.mockResolvedValue(null);
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 3 } });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps_create': 3 } });

await requireQuota('scraps', 'create')(req, res, next);

Expand All @@ -121,7 +121,7 @@ describe('requireQuota middleware:', () => {
'should treat %s subscription as free plan',
async (status) => {
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'starter', status });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 3 } });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps_create': 3 } });

await requireQuota('scraps', 'create')(req, res, next);

Expand All @@ -145,7 +145,7 @@ describe('requireQuota middleware:', () => {

test('should return correct error payload with upgradeUrl', async () => {
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'free', status: 'active' });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.execute': 100 } });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps_execute': 100 } });

await requireQuota('scraps', 'execute')(req, res, next);

Expand Down Expand Up @@ -178,7 +178,7 @@ describe('requireQuota middleware:', () => {

test('should use subscription plan when status is trialing', async () => {
mockSubscriptionRepository.findByOrganization.mockResolvedValue({ plan: 'starter', status: 'trialing' });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps.create': 15 } });
mockBillingUsageService.get.mockResolvedValue({ counters: { 'scraps_create': 15 } });

await requireQuota('scraps', 'create')(req, res, next);

Expand Down
16 changes: 8 additions & 8 deletions modules/billing/tests/billing.usage.endpoint.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('Billing usage endpoint unit tests:', () => {

test('should return usage and limits for active subscription', async () => {
mockBillingService.getSubscription.mockResolvedValue({ plan: 'starter', status: 'active' });
mockBillingUsageService.get.mockResolvedValue({ month: '2026-03', counters: { 'documents.create': 5, 'requests.execute': 42 } });
mockBillingUsageService.get.mockResolvedValue({ month: '2026-03', counters: { 'documents_create': 5, 'requests_execute': 42 } });

const req = { organization: { _id: orgId } };
await billingController.getUsage(req, res);
Expand All @@ -74,8 +74,8 @@ describe('Billing usage endpoint unit tests:', () => {
message: 'billing usage',
data: expect.objectContaining({
plan: 'starter',
usage: { 'documents.create': 5, 'requests.execute': 42 },
limits: { 'documents.create': 20, 'requests.execute': 2000 },
usage: { 'documents_create': 5, 'requests_execute': 42 },
limits: { 'documents_create': 20, 'requests_execute': 2000 },
}),
}));
});
Expand All @@ -92,7 +92,7 @@ describe('Billing usage endpoint unit tests:', () => {
type: 'success',
data: expect.objectContaining({
plan: 'free',
limits: { 'documents.create': 5, 'requests.execute': 100 },
limits: { 'documents_create': 5, 'requests_execute': 100 },
}),
}));
});
Expand All @@ -106,7 +106,7 @@ describe('Billing usage endpoint unit tests:', () => {
'paused',
])('should return free plan when subscription status is %s', async (status) => {
mockBillingService.getSubscription.mockResolvedValue({ plan: 'starter', status });
mockBillingUsageService.get.mockResolvedValue({ month: '2026-03', counters: { 'documents.create': 2 } });
mockBillingUsageService.get.mockResolvedValue({ month: '2026-03', counters: { 'documents_create': 2 } });

const req = { organization: { _id: orgId } };
await billingController.getUsage(req, res);
Expand All @@ -115,7 +115,7 @@ describe('Billing usage endpoint unit tests:', () => {
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
plan: 'free',
limits: { 'documents.create': 5, 'requests.execute': 100 },
limits: { 'documents_create': 5, 'requests_execute': 100 },
}),
}));
});
Expand All @@ -131,7 +131,7 @@ describe('Billing usage endpoint unit tests:', () => {
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
plan: 'starter',
limits: { 'documents.create': 20, 'requests.execute': 2000 },
limits: { 'documents_create': 20, 'requests_execute': 2000 },
}),
}));
});
Expand Down Expand Up @@ -163,7 +163,7 @@ describe('Billing usage endpoint unit tests:', () => {
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
plan: 'pro',
limits: { 'documents.create': null, 'requests.execute': null },
limits: { 'documents_create': null, 'requests_execute': null },
}),
}));
});
Expand Down
16 changes: 12 additions & 4 deletions modules/home/tests/home.integration.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('Home integration tests:', () => {
let agent;
let HomeService;
let adminToken;
let adminUser;
let originalOrganizationsEnabled;

// init
Expand All @@ -41,15 +42,16 @@ describe('Home integration tests:', () => {

// Create admin user and sign JWT for health endpoint test
const User = mongoose.model('User');
const admin = await User.create({
await User.deleteOne({ email: 'admin-home-health@test.com' });
adminUser = await User.create({
firstName: 'Admin',
lastName: 'Health',
email: 'admin-health@test.com',
email: 'admin-home-health@test.com',
Comment thread
PierreBrisorgueil marked this conversation as resolved.
password: 'W@os.jsI$Aw3$0m3',
provider: 'local',
roles: ['admin'],
});
adminToken = jwt.sign({ userId: admin.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
adminToken = jwt.sign({ userId: adminUser.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
Expand Down Expand Up @@ -204,10 +206,16 @@ describe('Home integration tests:', () => {
});
});

// Mongoose disconnect
// Cleanup and mongoose disconnect
afterAll(async () => {
jest.restoreAllMocks();
config.organizations.enabled = originalOrganizationsEnabled;
try {
if (adminUser) {
const User = mongoose.model('User');
await User.deleteOne({ _id: adminUser._id });
}
} catch (_) { /* cleanup – ignore errors */ }
try {
await mongooseService.disconnect();
} catch (err) {
Expand Down
Loading