Skip to content
44 changes: 43 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ jobs:
validate:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:14
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: teachlink_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

redis:
image: redis:6
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -32,8 +57,25 @@ jobs:
- name: Run lint
run: pnpm run lint:ci

- name: Check format
run: pnpm run format:check

- name: Check TypeScript errors
run: pnpm run typecheck

- name: Build application
run: pnpm run build
run: pnpm run build

- name: Run unit tests
run: pnpm run test:ci

- name: Run E2E tests
run: pnpm run test:e2e
env:
DATABASE_HOST: localhost
DATABASE_PORT: 5432
DATABASE_USER: postgres
DATABASE_PASSWORD: postgres
DATABASE_NAME: teachlink_test
REDIS_HOST: localhost
REDIS_PORT: 6379
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ApiVersionMiddleware } from './common/middleware/api-version.middleware
import { DeepLinkModule } from './deep-link/deep-link.module';
import { InvoicesModule } from './payments/invoices/invoices.module';
import { ReportingModule } from './payments/reporting/reporting.module';
import { PaymentsModule } from './payments/payments.module';
import { HealthModule } from './health/health.module';
import { QueueModule } from './queues/queue.module';
import { WorkersBridgeModule } from './workers/bridge/workers-bridge.module';
Expand Down Expand Up @@ -65,6 +66,7 @@ const featureFlags = loadFeatureFlags();
DeepLinkModule,
InvoicesModule,
ReportingModule,
PaymentsModule,
HealthModule,
QueueModule,
WorkersBridgeModule,
Expand Down
2 changes: 2 additions & 0 deletions src/audit-log/enums/audit-action.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export enum AuditAction {
DATA_RETENTION_APPLIED = 'DATA_RETENTION_APPLIED',
AUDIT_LOG_EXPORTED = 'AUDIT_LOG_EXPORTED',
REPORT_GENERATED = 'REPORT_GENERATED',
// Payment reconciliation
PAYMENT_RECONCILIATION_MISMATCH = 'PAYMENT_RECONCILIATION_MISMATCH',
}
export enum AuditSeverity {
INFO = 'INFO',
Expand Down
16 changes: 12 additions & 4 deletions src/payments/payments.module.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CurrencyModule } from '../currency/currency.module';
import { AuditLogModule } from '../audit-log/audit-log.module';
import { Payment } from './entities/payment.entity';
import { Subscription } from './entities/subscription.entity';
import { Invoice } from './entities/invoice.entity';
import { Refund } from './entities/refund.entity';
import { PricingService } from './services/pricing.service';
import { PricingController } from './controllers/pricing.controller';
import { ReconciliationService } from './reconciliation/reconciliation.service';
import { ReconciliationTask } from './reconciliation/reconciliation.task';
import { ReconciliationController } from './reconciliation/reconciliation.controller';

@Module({
imports: [TypeOrmModule.forFeature([Payment, Subscription, Invoice, Refund]), CurrencyModule],
providers: [PricingService],
controllers: [PricingController],
exports: [PricingService, CurrencyModule],
imports: [
TypeOrmModule.forFeature([Payment, Subscription, Invoice, Refund]),
CurrencyModule,
AuditLogModule,
],
providers: [PricingService, ReconciliationService, ReconciliationTask],
controllers: [PricingController, ReconciliationController],
exports: [PricingService, CurrencyModule, ReconciliationService],
})
export class PaymentsModule {}
62 changes: 62 additions & 0 deletions src/payments/reconciliation/reconciliation.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { ReconciliationService, ReconciliationReport } from './reconciliation.service';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/decorators/roles.decorator';

/**
* Controller for payment reconciliation endpoints.
* Provides admin-only access to reconciliation reports.
*/
@ApiTags('Payments - Reconciliation')
@ApiBearerAuth()
@Controller('payments/reconciliation')
@UseGuards(RolesGuard)
export class ReconciliationController {
constructor(private readonly reconciliationService: ReconciliationService) {}

/**
* Get the last reconciliation report
* GET /payments/reconciliation/report
*/
@Get('report')
@Roles('admin')
@ApiOperation({
summary: 'Get last reconciliation report',
description:
'Returns the results of the most recent payment reconciliation run. Admin-only endpoint.',
})
@ApiResponse({
status: 200,
description: 'Reconciliation report retrieved successfully',
schema: {
type: 'object',
properties: {
runAt: { type: 'string', format: 'date-time' },
startDate: { type: 'string', format: 'date-time' },
endDate: { type: 'string', format: 'date-time' },
totalProviderTransactions: { type: 'number' },
totalLocalPayments: { type: 'number' },
matchedTransactions: { type: 'number' },
unmatchedProviderTransactions: { type: 'array', items: { type: 'object' } },
unmatchedLocalPayments: { type: 'array', items: { type: 'object' } },
mismatches: { type: 'array', items: { type: 'object' } },
},
},
})
@ApiResponse({
status: 401,
description: 'Unauthorized - authentication required',
})
@ApiResponse({
status: 403,
description: 'Forbidden - admin role required',
})
@ApiResponse({
status: 404,
description: 'No reconciliation report available',
})
getLastReport(): ReconciliationReport | null {
return this.reconciliationService.getLastReport();
}
}
Loading
Loading