diff --git a/.gitea/workflows/build-and-deploy.yml b/.gitea/workflows/build-and-deploy.yml index 88a1397..9d02c2f 100644 --- a/.gitea/workflows/build-and-deploy.yml +++ b/.gitea/workflows/build-and-deploy.yml @@ -124,13 +124,23 @@ jobs: node -e "require('@rollup/rollup-linux-arm64-gnu')" fi - - run: npm run db:generate - - run: npm run type-check - - run: npm run test:api - - run: npm run test:frontends - - run: npm run db:deploy - - run: npx prisma db push --schema packages/database/prisma/schema.prisma - - run: npm run test:api:integration + - name: Generate database client + run: npm run db:generate + + - name: Type check + run: npm run type-check + + - name: Unit tests + run: npm run test:unit + + - name: Apply test database migrations + run: npm run db:deploy + + - name: Synchronize test database schema + run: npx prisma db push --schema packages/database/prisma/schema.prisma + + - name: Integration tests + run: npm run test:integration build-image: name: Build & Push Docker Image diff --git a/apps/Archive.zip b/apps/Archive.zip new file mode 100644 index 0000000..2ced688 Binary files /dev/null and b/apps/Archive.zip differ diff --git a/apps/api/src/modules/subscriptions/subscription.routes.ts b/apps/api/src/modules/subscriptions/subscription.routes.ts index 673f1eb..5b46578 100644 --- a/apps/api/src/modules/subscriptions/subscription.routes.ts +++ b/apps/api/src/modules/subscriptions/subscription.routes.ts @@ -74,18 +74,9 @@ webhookRouter.post('/webhooks/stripe', async (req, res, next) => { } catch (err) { next(err) } }) -// ─── PayPal capture (auth but no subscription check) ────────── +// ─── Authenticated billing recovery/self-service ─────────────── -router.post('/capture-paypal', requireCompanyAuth, requireTenant, requireSubscriptionFull, requireRole('OWNER'), async (req, res, next) => { - try { - const { paypalOrderId } = parseBody(capturePaypalSchema, req) - ok(res, await service.capturePaypal(req.companyId, paypalOrderId)) - } catch (err) { next(err) } -}) - -// ─── Authenticated ───────────────────────────────────────────── - -router.use(requireCompanyAuth, requireTenant, requireSubscriptionRead) +router.use(requireCompanyAuth, requireTenant) router.get('/me', async (req, res, next) => { try { ok(res, await service.getSubscription(req.companyId)) } catch (err) { next(err) } @@ -103,34 +94,45 @@ router.get('/entitlement', async (req, res, next) => { try { ok(res, await service.getEntitlement(req.companyId)) } catch (err) { next(err) } }) -router.post('/trial', requireSubscriptionFull, requireRole('OWNER'), async (req, res, next) => { +router.post('/trial', requireRole('OWNER'), async (req, res, next) => { try { const body = parseBody(startTrialSchema, req) ok(res, await service.startTrial(req.companyId, body.plan, body.billingPeriod, body.currency)) } catch (err) { next(err) } }) -router.post('/checkout', requireSubscriptionFull, requireRole('OWNER'), async (req, res, next) => { +router.post('/checkout', requireRole('OWNER'), async (req, res, next) => { try { const body = parseBody(checkoutSchema, req) ok(res, await service.checkout(req.companyId, body)) } catch (err) { next(err) } }) -router.post('/reactivate', requireSubscriptionFull, requireRole('OWNER'), async (req, res, next) => { +router.post('/reactivate', requireRole('OWNER'), async (req, res, next) => { try { const body = parseBody(reactivateSchema, req) ok(res, await service.reactivate(req.companyId, body)) } catch (err) { next(err) } }) -router.post('/change-plan', requireSubscriptionFull, requireRole('OWNER'), async (req, res, next) => { +router.post('/change-plan', requireRole('OWNER'), async (req, res, next) => { try { const body = parseBody(changePlanSchema, req) ok(res, await service.changePlan(req.companyId, body)) } catch (err) { next(err) } }) +router.post('/capture-paypal', requireRole('OWNER'), async (req, res, next) => { + try { + const { paypalOrderId } = parseBody(capturePaypalSchema, req) + ok(res, await service.capturePaypal(req.companyId, paypalOrderId)) + } catch (err) { next(err) } +}) + +// ─── Active subscription actions ─────────────────────────────── + +router.use(requireSubscriptionRead) + router.post('/cancel', requireSubscriptionFull, requireRole('OWNER'), async (req, res, next) => { try { const { mode, reason } = parseBody(cancelSchema, req) diff --git a/apps/api/src/tests/integration/subscriptions.test.ts b/apps/api/src/tests/integration/subscriptions.test.ts index d8265b1..ce439cc 100644 --- a/apps/api/src/tests/integration/subscriptions.test.ts +++ b/apps/api/src/tests/integration/subscriptions.test.ts @@ -128,6 +128,26 @@ describe('Subscriptions API', () => { expect(res.body.data.some((item: any) => item.id === invoice.id)).toBe(true) }) + it('allows expired subscriptions to read billing recovery data', async () => { + const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' }) + const subscription = await prisma.subscription.update({ + where: { companyId: company.id }, + data: { status: 'EXPIRED' }, + }) + const invoice = await createSubscriptionInvoice(company.id, subscription.id) + const token = signEmployeeToken(employee.id, company.id, 'OWNER') + + const [subscriptionRes, invoicesRes] = await Promise.all([ + request(app).get('/api/v1/subscriptions/me').set(authHeader(token)), + request(app).get('/api/v1/subscriptions/invoices').set(authHeader(token)), + ]) + + expect(subscriptionRes.status).toBe(200) + expect(subscriptionRes.body.data.status).toBe('EXPIRED') + expect(invoicesRes.status).toBe(200) + expect(invoicesRes.body.data.some((item: any) => item.id === invoice.id)).toBe(true) + }) + it('changes the subscription plan for OWNER', async () => { const res = await request(app) .post('/api/v1/subscriptions/change-plan') diff --git a/apps/homepage/package.json b/apps/homepage/package.json index 83b70c8..3eafbb3 100644 --- a/apps/homepage/package.json +++ b/apps/homepage/package.json @@ -8,6 +8,7 @@ "start": "next start -H 0.0.0.0 -p 3000", "type-check": "tsc --noEmit", "test": "vitest run", + "test:integration": "vitest run --config vitest.integration.config.ts", "test:watch": "vitest", "lint": "eslint . --max-warnings=0", "format": "prettier --write .", diff --git a/package.json b/package.json index 66b92bd..b6993c4 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,11 @@ "test:dashboard": "npm run test --workspace @rentaldrivego/dashboard", "test:admin": "npm run test --workspace @rentaldrivego/admin", "test:homepage": "npm run test --workspace @rentaldrivego/homepage", + "test:homepage:integration": "npm run test:integration --workspace @rentaldrivego/homepage", "test:carplace": "npm run test --workspace @rentaldrivego/carplace", "test:frontends": "npm run test:dashboard && npm run test:admin && npm run test:homepage && npm run test:carplace", + "test:unit": "npm run test:api && npm run test:frontends", + "test:integration": "npm run test:api:integration && npm run test:homepage:integration", "security:static": "node scripts/security-static-check.mjs", "security:scan": "npm run security:static && npm audit --production" },