mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge branch 'alpha' of https://github.com/Tria-plc/edr-platform into alpha
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/.github
|
||||
**/.vscode
|
||||
**/.git
|
||||
**/.env
|
||||
.env
|
||||
96
.github/workflows/deploy.yaml
vendored
Normal file
96
.github/workflows/deploy.yaml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: Automatic Deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
environment:
|
||||
name: 🌍 Setup Environment
|
||||
runs-on: [self-hosted]
|
||||
outputs:
|
||||
target: ${{ steps.dev.outputs.target || steps.staging.outputs.target }}
|
||||
steps:
|
||||
- name: Verify NPM Token
|
||||
env:
|
||||
# We map the secret here to check its existence
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$NPM_TOKEN" ]; then
|
||||
echo "::error::The NPM_TOKEN secret is missing or empty. Please add it to your GitHub Secrets."
|
||||
exit 1
|
||||
fi
|
||||
echo "NPM_TOKEN is present, proceeding with build..."
|
||||
|
||||
- name: 🛠️ Set Development Environment
|
||||
id: dev
|
||||
if: ${{github.ref_name == 'dev'}}
|
||||
run: |
|
||||
echo "target=dev" >> $GITHUB_OUTPUT
|
||||
- name: 🚀 Set Staging Environment
|
||||
id: staging
|
||||
if: ${{github.ref_name == 'staging'}}
|
||||
run: |
|
||||
echo "target=staging" >> $GITHUB_OUTPUT
|
||||
|
||||
build-base-image:
|
||||
name: 🏗️ Build Base Image
|
||||
runs-on: [self-hosted, dev]
|
||||
needs: [environment]
|
||||
steps:
|
||||
- name: 🔍 Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🐳 Build Docker Image
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Create the multi-line file
|
||||
cat <<EOF > .npmrc_temp
|
||||
@tria-plc:registry=https://npm.pkg.github.com
|
||||
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
|
||||
always-auth=true
|
||||
EOF
|
||||
|
||||
# Build using the file
|
||||
docker build --secret id=npmrc,src=.npmrc_temp -t edr-${{needs.environment.outputs.target}} .
|
||||
docker build --secret id=npmrc,src=.npmrc_temp --target passenger-migration -t edr-passenger-migration-${{needs.environment.outputs.target}} .
|
||||
|
||||
# Shred/Remove the sensitive file
|
||||
rm .npmrc_temp
|
||||
|
||||
deploy-service:
|
||||
name: ${{ matrix.display_name }}
|
||||
runs-on: [self-hosted, dev]
|
||||
needs: [build-base-image, environment]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- service: freight-api
|
||||
env_file: .env.freight-api
|
||||
display_name: 🚚 Deploy Freight API Service
|
||||
- service: passenger-api
|
||||
env_file: .env.passenger-api
|
||||
display_name: 🧑🦲 Deploy Passenger API Service
|
||||
|
||||
steps:
|
||||
- name: 🔍 Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 📋 Copy ${{ matrix.service }} Environment
|
||||
run: cp ~/environment/edr/${{needs.environment.outputs.target}}/${{ matrix.env_file }} .env
|
||||
|
||||
- name: 🧪 Run Passenger API migrations
|
||||
if: ${{ matrix.service == 'passenger-api' }}
|
||||
run: |
|
||||
docker run --rm --env-file .env edr-passenger-migration-${{needs.environment.outputs.target}}
|
||||
|
||||
- name: 🚀 Start ${{ matrix.service }} Service
|
||||
run: docker compose --project-name="edr-${{needs.environment.outputs.target}}" up -d --force-recreate ${{ matrix.service }} --build
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,4 +21,5 @@ coverage/
|
||||
# OS/editor
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
.vscode/
|
||||
.npmrc
|
||||
84
Dockerfile
Normal file
84
Dockerfile
Normal file
@@ -0,0 +1,84 @@
|
||||
FROM node:24.15.0 AS base
|
||||
RUN corepack enable && corepack prepare pnpm@latest-11 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
|
||||
COPY apps/edr-freight-api/package.json ./apps/edr-freight-api/
|
||||
COPY apps/edr-passenger-api/package.json ./apps/edr-passenger-api/
|
||||
|
||||
COPY apps/edr-freight-web/backoffice/package.json ./apps/edr-freight-web/backoffice/
|
||||
COPY apps/edr-freight-web/portal/package.json ./apps/edr-freight-web/portal/
|
||||
|
||||
COPY apps/edr-passenger-web/backoffice/package.json ./apps/edr-passenger-web/backoffice/
|
||||
COPY apps/edr-passenger-web/portal/package.json ./apps/edr-passenger-web/portal/
|
||||
|
||||
|
||||
COPY packages/api-common/package.json packages/api-common/
|
||||
|
||||
COPY packages/config/eslint-config/package.json packages/config/eslint-config/
|
||||
COPY packages/config/prettier-config/package.json packages/config/prettier-config/
|
||||
COPY packages/config/tsconfig/package.json packages/config/tsconfig/
|
||||
|
||||
COPY packages/types/package.json packages/types/
|
||||
COPY packages/ui-common/package.json packages/ui-common/
|
||||
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store\
|
||||
--mount=type=secret,id=npmrc,target=./.npmrc \
|
||||
pnpm install --frozen-lockfile
|
||||
FROM deps AS build
|
||||
COPY . .
|
||||
|
||||
RUN pnpm run build
|
||||
|
||||
FROM base AS freight-api
|
||||
# RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app/apps/edr-freight-api
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY --from=deps /app/node_modules ./../../node_modules
|
||||
COPY --from=deps /app/apps/edr-freight-api/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/edr-freight-api/dist ./dist
|
||||
COPY --from=build /app/apps/edr-freight-api/package.json ./package.json
|
||||
COPY --from=build /app/packages ./../../packages
|
||||
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/main.js"]
|
||||
|
||||
|
||||
FROM base AS passenger-api
|
||||
RUN apt-get update -y && apt-get install -y openssl
|
||||
# RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app/apps/edr-passenger-api
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Use build-stage node_modules (not deps): `pnpm run build` runs `prisma generate`, which
|
||||
# writes the real @prisma/client (enums, types). deps never runs generate, so @IsEnum(ServiceClass)
|
||||
# and similar would see undefined at runtime if we copied deps only.
|
||||
COPY --from=build /app/node_modules ./../../node_modules
|
||||
COPY --from=build /app/apps/edr-passenger-api/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/edr-passenger-api/dist ./dist
|
||||
COPY --from=build /app/apps/edr-passenger-api/package.json ./package.json
|
||||
COPY --from=build /app/packages ./../../packages
|
||||
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/main.js"]
|
||||
|
||||
FROM build as passenger-migration
|
||||
WORKDIR /app/apps/edr-passenger-api
|
||||
CMD pnpm run prisma:migrate && pnpm run prisma:seed
|
||||
|
||||
|
||||
|
||||
FROM nginx:1.27-alpine AS freight-web-portal
|
||||
COPY --from=build /app/apps/edr-freight-web/portal/dist /usr/share/nginx/html
|
||||
EXPOSE 5173
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
|
||||
FROM nginx:1.27-alpine AS freight-web-backoffice
|
||||
COPY --from=build /app/apps/edr-freight-web/backoffice/dist /usr/share/nginx/html
|
||||
EXPOSE 5173
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,26 +0,0 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/edr-freight-api/package.json ./apps/edr-freight-api/
|
||||
COPY packages ./packages
|
||||
RUN pnpm install --frozen-lockfile --filter @edr/freight-api...
|
||||
|
||||
FROM deps AS build
|
||||
COPY apps/edr-freight-api ./apps/edr-freight-api
|
||||
RUN pnpm --filter @edr/freight-api build
|
||||
|
||||
FROM node:20-alpine AS runtime
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app/apps/edr-freight-api
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY --from=deps /app/node_modules ./../../node_modules
|
||||
COPY --from=deps /app/apps/edr-freight-api/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/edr-freight-api/dist ./dist
|
||||
COPY --from=build /app/apps/edr-freight-api/package.json ./package.json
|
||||
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/main.js"]
|
||||
@@ -1,18 +0,0 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/edr-freight-web/backoffice/package.json ./apps/edr-freight-web/backoffice/
|
||||
COPY packages ./packages
|
||||
RUN pnpm install --frozen-lockfile --filter @edr/freight-backoffice...
|
||||
|
||||
FROM deps AS build
|
||||
COPY apps/edr-freight-web/backoffice ./apps/edr-freight-web/backoffice
|
||||
RUN pnpm --filter @edr/freight-backoffice build
|
||||
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
COPY --from=build /app/apps/edr-freight-web/backoffice/dist /usr/share/nginx/html
|
||||
EXPOSE 5183
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,18 +0,0 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/edr-freight-web/portal/package.json ./apps/edr-freight-web/portal/
|
||||
COPY packages ./packages
|
||||
RUN pnpm install --frozen-lockfile --filter @edr/freight-portal...
|
||||
|
||||
FROM deps AS build
|
||||
COPY apps/edr-freight-web/portal ./apps/edr-freight-web/portal
|
||||
RUN pnpm --filter @edr/freight-portal build
|
||||
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
COPY --from=build /app/apps/edr-freight-web/portal/dist /usr/share/nginx/html
|
||||
EXPOSE 5173
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,28 +0,0 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/edr-passenger-api/package.json ./apps/edr-passenger-api/
|
||||
COPY packages ./packages
|
||||
RUN pnpm install --frozen-lockfile --filter @edr/passenger-api...
|
||||
|
||||
FROM deps AS build
|
||||
COPY apps/edr-passenger-api ./apps/edr-passenger-api
|
||||
RUN pnpm --filter @edr/passenger-api run prisma:generate
|
||||
RUN pnpm --filter @edr/passenger-api build
|
||||
|
||||
FROM node:20-alpine AS runtime
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app/apps/edr-passenger-api
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY --from=deps /app/node_modules ./../../node_modules
|
||||
COPY --from=deps /app/apps/edr-passenger-api/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/edr-passenger-api/dist ./dist
|
||||
COPY --from=build /app/apps/edr-passenger-api/package.json ./package.json
|
||||
COPY --from=build /app/apps/edr-passenger-api/prisma ./prisma
|
||||
|
||||
EXPOSE 4000
|
||||
CMD ["node", "dist/main.js"]
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"plugins": ["@nestjs/swagger"],
|
||||
"tsConfigPath": "tsconfig.build.json",
|
||||
"watchAssets": true
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nest start --watch",
|
||||
"build": "nest build",
|
||||
"build": "prisma generate && nest build",
|
||||
"start": "node dist/main.js",
|
||||
"start:prod": "node dist/main.js",
|
||||
"lint": "eslint src",
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
import { ResponseTransformInterceptor } from './common/interceptors/response-transform.interceptor';
|
||||
import { SessionActivityInterceptor } from './common/interceptors/session-activity.interceptor';
|
||||
import "reflect-metadata";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { ValidationPipe } from "@nestjs/common";
|
||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
import { AppModule } from "./app.module";
|
||||
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
|
||||
import { ResponseTransformInterceptor } from "./common/interceptors/response-transform.interceptor";
|
||||
import { SessionActivityInterceptor } from "./common/interceptors/session-activity.interceptor";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.enableCors({
|
||||
origin: [
|
||||
process.env.FRONTEND_URL ?? 'http://localhost:3000',
|
||||
process.env.PORTAL_URL ?? 'http://localhost:3001',
|
||||
process.env.FRONTEND_URL ?? "http://localhost:3000",
|
||||
process.env.PORTAL_URL ?? "http://localhost:3001",
|
||||
],
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ async function bootstrap() {
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('EDR Passenger API')
|
||||
.setTitle("EDR Passenger API")
|
||||
.setDescription(
|
||||
`# Ethio-Djibouti Railway Passenger Booking API
|
||||
|
||||
@@ -205,78 +205,39 @@ Payment providers send notifications to:
|
||||
- **Status Page:** https://status.edr-platform.com
|
||||
`,
|
||||
)
|
||||
.setVersion('1.0.0')
|
||||
.setVersion("1.0.0")
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
in: 'header',
|
||||
description: 'JWT token for passenger authentication. Obtain via POST /auth/login'
|
||||
},
|
||||
'JWT-auth'
|
||||
{ type: "http", scheme: "bearer", bearerFormat: "JWT", in: "header" },
|
||||
"JWT-auth",
|
||||
)
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
in: 'header',
|
||||
description: 'Corporate IAM token for back-office operations (agents, fraud, reports)'
|
||||
},
|
||||
'IAM-auth'
|
||||
)
|
||||
.addTag('Auth', '🔐 Registration, login, OTP verification, password reset')
|
||||
.addTag('Agents', '👨💼 Agent booking, shifts, commissions, reconciliation')
|
||||
.addTag('Booking', '🎫 Booking lifecycle, modification, cancellation, refunds')
|
||||
.addTag('Dashboard', '📊 Home dashboard aggregated data')
|
||||
.addTag('Fleet', '🚂 Trains, physical coaches, seat auto-generation, coach-to-schedule assignments')
|
||||
.addTag('Seat Classes', '🎨 Seat class management and configuration')
|
||||
.addTag('Fraud Detection', '🔒 Fraud detection, risk scoring, user blocking')
|
||||
.addTag('Live Tracking', '📍 Real-time trip status, location updates, crowd signals')
|
||||
.addTag('Loyalty', '🏆 Points accumulation, tier management, rewards redemption')
|
||||
.addTag('Notifications', '🔔 Push, email, SMS notifications, preferences')
|
||||
.addTag('Passenger', '👤 Profiles, traveler profiles, saved routes, preferences')
|
||||
.addTag('Payment', '💳 Payment intents, status queries, refunds')
|
||||
.addTag('Payment Webhooks', '🔗 Payment provider callback endpoints')
|
||||
.addTag('Promotions', '🎁 Promo codes, campaigns, discount validation')
|
||||
.addTag('Reports', '📈 Revenue reports, occupancy analytics, agent sales')
|
||||
.addTag('Routes', '🗺️ Reusable route templates with ordered stops — referenced by schedules')
|
||||
.addTag('Schedule', '🗓️ Train schedules (created from routes), stop time management, fare rules')
|
||||
.addTag('Search', '🔍 Trip search, availability, fare quotes')
|
||||
.addTag('Seats', '🪑 Seat maps, holds, releases, blocking, auto-assign')
|
||||
.addTag('Segment-based Seats', '🎯 Segment-based seat availability and booking')
|
||||
.addTag('Stations', '🚉 Station directory, information, crowd signals')
|
||||
.addTag('Support', '💬 FAQ management, live chat conversations')
|
||||
.addTag('Tickets', '🎟️ QR/barcode generation, PDF tickets, gate validation')
|
||||
.addTag('Wallet', '💰 Wallet balance, top-up, transaction ledger')
|
||||
.addServer('http://localhost:4000', 'Local Development')
|
||||
.addServer('https://api-staging.edr-platform.com', 'Staging Environment')
|
||||
.addServer('https://api.edr-platform.com', 'Production')
|
||||
.addTag("Auth", "Registration and login")
|
||||
.addTag("Stations", "Station directory")
|
||||
.addTag("Fleet", "Train services and coaches")
|
||||
.addTag("Schedule", "Trips and fare rules")
|
||||
.addTag("Search", "Trip search and fare quotes")
|
||||
.addTag("Seats", "Seat maps and holds")
|
||||
.addTag("Booking", "Booking lifecycle")
|
||||
.addTag("Payment", "Payment intents and refunds")
|
||||
.addTag("Tickets", "QR ticket generation and validation")
|
||||
.addTag("Passenger", "Profiles, traveler profiles, saved routes")
|
||||
.addTag("Notifications", "Push and email notifications")
|
||||
.addTag("Loyalty", "Points, tiers, and rewards")
|
||||
.addTag("Wallet", "Wallet balance and ledger")
|
||||
.addTag("Promotions", "Promo codes and campaigns")
|
||||
.addTag("Live Tracking", "Real-time trip status and crowd signals")
|
||||
.addTag("Support", "FAQ and chat support")
|
||||
.addTag("Dashboard", "Home dashboard aggregate")
|
||||
//.addServer('http://localhost:4000', 'Development')
|
||||
// .addServer("https://api.edr-platform.com", "Production")
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api-docs', app, document, {
|
||||
customSiteTitle: 'EDR Passenger API Documentation',
|
||||
customfavIcon: 'https://edr-platform.com/favicon.ico',
|
||||
customCss: `
|
||||
.swagger-ui .topbar { display: none }
|
||||
.swagger-ui .info { margin: 20px 0 }
|
||||
.swagger-ui .info .title { font-size: 36px; font-weight: bold }
|
||||
.swagger-ui .scheme-container { background: #fafafa; padding: 15px; border-radius: 4px }
|
||||
`,
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
docExpansion: 'none',
|
||||
SwaggerModule.setup("api-docs", app, document, {
|
||||
customSiteTitle: "EDR Passenger API",
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
docExpansion: "none",
|
||||
filter: true,
|
||||
tagsSorter: 'alpha',
|
||||
operationsSorter: 'alpha',
|
||||
displayRequestDuration: true,
|
||||
tryItOutEnabled: true,
|
||||
syntaxHighlight: {
|
||||
activate: true,
|
||||
theme: 'monokai'
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsString, IsEnum, IsOptional } from 'class-validator';
|
||||
import { IsString, IsEnum, IsOptional, IsIn } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { PaymentIntentStatus } from '@prisma/client';
|
||||
|
||||
@@ -11,14 +11,20 @@ export enum PaymentMethodTypeEnum {
|
||||
WALLET = 'WALLET' // Internal
|
||||
}
|
||||
|
||||
export type PaymentPlatformDto = 'web' | 'mobile';
|
||||
|
||||
export class InitiatePaymentDto {
|
||||
@ApiProperty({ example: 'booking-uuid' }) @IsString() bookingId: string;
|
||||
@ApiProperty({
|
||||
@ApiProperty({
|
||||
enum: PaymentMethodTypeEnum,
|
||||
description: 'Payment method: TELEBIRR/CBE_BIRR/EBIRR (Ethiopia), WAAFI (Djibouti), CARD (International), WALLET (Internal)',
|
||||
example: 'TELEBIRR'
|
||||
}) @IsEnum(PaymentMethodTypeEnum) method: PaymentMethodTypeEnum;
|
||||
@ApiPropertyOptional({ description: 'Saved payment method ID (optional)' }) @IsOptional() @IsString() paymentMethodId?: string;
|
||||
@ApiPropertyOptional({ enum: ['web', 'mobile'], default: 'web', description: 'Payment platform (web or mobile)' })
|
||||
@IsOptional()
|
||||
@IsIn(['web', 'mobile'])
|
||||
platform?: PaymentPlatformDto;
|
||||
}
|
||||
|
||||
export class RefundDto {
|
||||
@@ -34,8 +40,11 @@ export class AddPaymentMethodDto {
|
||||
}
|
||||
|
||||
export class ClientActionDto {
|
||||
@ApiProperty({ enum: ['REDIRECT'] }) type: 'REDIRECT';
|
||||
@ApiProperty() url: string;
|
||||
@ApiProperty({ enum: ['REDIRECT', 'LAUNCH_APP'] }) type: 'REDIRECT' | 'LAUNCH_APP';
|
||||
@ApiPropertyOptional({ description: 'Set when type=REDIRECT (web flow)' }) url?: string;
|
||||
@ApiPropertyOptional({ description: 'Set when type=LAUNCH_APP (mobile flow)' }) prepayId?: string;
|
||||
@ApiPropertyOptional({ description: 'Set when type=LAUNCH_APP (mobile flow)' }) receiveCode?: string;
|
||||
@ApiPropertyOptional({ description: 'Set when type=LAUNCH_APP (mobile flow)' }) shortCode?: string;
|
||||
}
|
||||
|
||||
export class InitiateResponseDto {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TicketsService } from '../tickets/tickets.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Prisma, PaymentIntentStatus, PaymentMethodType } from '@prisma/client';
|
||||
import { InitiatePaymentDto, RefundDto, AddPaymentMethodDto, InitiateResponseDto, IntentStatusDto } from './payments.dto';
|
||||
import { PaymentProvider, ProviderStatus } from './payments.types';
|
||||
import { ClientAction, PaymentProvider, ProviderStatus } from './payments.types';
|
||||
import { TelebirrProvider } from './providers/telebirr.provider';
|
||||
import { CbeBirrProvider } from './providers/cbe-birr.provider';
|
||||
import { EBirrProvider } from './providers/ebirr.provider';
|
||||
@@ -69,7 +69,7 @@ export class PaymentsService {
|
||||
|
||||
const provider = this.providers.get(method);
|
||||
if (provider) {
|
||||
return this.initiateProviderPayment(booking, provider);
|
||||
return this.initiateProviderPayment(booking, provider, dto.platform);
|
||||
}
|
||||
|
||||
throw new BadRequestException(`Unsupported payment method: ${method}`);
|
||||
@@ -142,6 +142,7 @@ export class PaymentsService {
|
||||
private async initiateProviderPayment(
|
||||
booking: Prisma.BookingGetPayload<{ include: { seats: true } }>,
|
||||
provider: PaymentProvider,
|
||||
platform: 'web' | 'mobile' | undefined,
|
||||
): Promise<InitiateResponseDto> {
|
||||
const merchantOrderId = createMerchantOrderId();
|
||||
const result = await provider.initiate({
|
||||
@@ -149,6 +150,7 @@ export class PaymentsService {
|
||||
bookingRef: booking.bookingRef,
|
||||
amountMinor: booking.totalMinor,
|
||||
currency: booking.currency,
|
||||
platform,
|
||||
});
|
||||
|
||||
const intent = await this.prisma.paymentIntent.upsert({
|
||||
@@ -187,7 +189,7 @@ export class PaymentsService {
|
||||
): InitiateResponseDto {
|
||||
const clientAction =
|
||||
intent.clientAction && typeof intent.clientAction === 'object'
|
||||
? (intent.clientAction as unknown as { type: 'REDIRECT'; url: string })
|
||||
? (intent.clientAction as unknown as ClientAction)
|
||||
: undefined;
|
||||
return {
|
||||
intentId: intent.id,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { PaymentIntentStatus, PaymentMethodType } from '@prisma/client';
|
||||
|
||||
export interface ClientAction {
|
||||
type: 'REDIRECT' | 'NONE';
|
||||
url?: string;
|
||||
}
|
||||
export type PaymentPlatform = 'web' | 'mobile';
|
||||
|
||||
export type ClientAction =
|
||||
| { type: 'REDIRECT'; url: string }
|
||||
| { type: 'LAUNCH_APP'; prepayId: string; receiveCode?: string; shortCode: string };
|
||||
|
||||
export interface ProviderInitiationInput {
|
||||
merchantOrderId: string;
|
||||
bookingRef: string;
|
||||
amountMinor: number;
|
||||
currency: string;
|
||||
platform?: PaymentPlatform;
|
||||
}
|
||||
|
||||
export interface ProviderInitiationResult {
|
||||
|
||||
@@ -58,12 +58,21 @@ export class TelebirrProvider implements PaymentProvider {
|
||||
);
|
||||
}
|
||||
|
||||
const checkoutUrl = this.buildCheckoutUrl(prepayId);
|
||||
const expiresAt = this.computeExpiresAt(requestBody.biz_content.timeout_express);
|
||||
const platform = input.platform ?? 'web';
|
||||
const clientAction =
|
||||
platform === 'mobile'
|
||||
? {
|
||||
type: 'LAUNCH_APP' as const,
|
||||
prepayId,
|
||||
receiveCode: response.biz_content?.receiveCode,
|
||||
shortCode: this.merchantCode,
|
||||
}
|
||||
: { type: 'REDIRECT' as const, url: this.buildCheckoutUrl(prepayId) };
|
||||
|
||||
return {
|
||||
providerOrderId: prepayId,
|
||||
clientAction: { type: 'REDIRECT', url: checkoutUrl },
|
||||
clientAction,
|
||||
expiresAt,
|
||||
rawInitiation: {
|
||||
request: this.sanitize(requestBody),
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/edr-passenger-web/backoffice/package.json ./apps/edr-passenger-web/backoffice/
|
||||
COPY packages ./packages
|
||||
RUN pnpm install --frozen-lockfile --filter @edr/passenger-backoffice...
|
||||
|
||||
FROM deps AS build
|
||||
COPY apps/edr-passenger-web/backoffice ./apps/edr-passenger-web/backoffice
|
||||
RUN pnpm --filter @edr/passenger-backoffice build
|
||||
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
COPY --from=build /app/apps/edr-passenger-web/backoffice/dist /usr/share/nginx/html
|
||||
EXPOSE 5184
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,18 +0,0 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/edr-passenger-web/portal/package.json ./apps/edr-passenger-web/portal/
|
||||
COPY packages ./packages
|
||||
RUN pnpm install --frozen-lockfile --filter @edr/passenger-portal...
|
||||
|
||||
FROM deps AS build
|
||||
COPY apps/edr-passenger-web/portal ./apps/edr-passenger-web/portal
|
||||
RUN pnpm --filter @edr/passenger-portal build
|
||||
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
COPY --from=build /app/apps/edr-passenger-web/portal/dist /usr/share/nginx/html
|
||||
EXPOSE 5174
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -7,8 +7,8 @@ export default defineConfig({
|
||||
port: 5184,
|
||||
host: "0.0.0.0",
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
},
|
||||
// test: {
|
||||
// environment: "jsdom",
|
||||
// globals: true,
|
||||
// },
|
||||
});
|
||||
|
||||
@@ -7,8 +7,8 @@ export default defineConfig({
|
||||
port: 5174,
|
||||
host: "0.0.0.0",
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
},
|
||||
// test: {
|
||||
// environment: "jsdom",
|
||||
// globals: true,
|
||||
// },
|
||||
});
|
||||
|
||||
21
docker-compose.yaml
Normal file
21
docker-compose.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
freight-api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
target: freight-api
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- ${PORT}:${PORT}
|
||||
restart: unless-stopped
|
||||
passenger-api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
target: passenger-api
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- ${PORT}:${PORT}
|
||||
restart: unless-stopped
|
||||
@@ -42,7 +42,7 @@
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@9.12.0",
|
||||
"packageManager": "pnpm@11.1.1+sha512.d1fdf5f73c617b64fa1a56a81c3c8dfe0e966e33a6010aa256b517ae77be21d93e05affc0de1a83b0e4f29d569f68b446ae8f068cd7247c0bb3df0fb4d7bdf9a",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
|
||||
13317
pnpm-lock.yaml
generated
13317
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,21 @@
|
||||
packages:
|
||||
- "apps/edr-freight-api"
|
||||
- "apps/edr-freight-web/*"
|
||||
- "apps/edr-passenger-api"
|
||||
- "apps/edr-passenger-web/*"
|
||||
- "packages/*"
|
||||
- "packages/config/*"
|
||||
allowBuilds:
|
||||
'@nestjs/core': true
|
||||
'@prisma/client': true
|
||||
'@prisma/engines': true
|
||||
'@scarf/scarf': true
|
||||
argon2: true
|
||||
bcrypt: true
|
||||
core-js: true
|
||||
es5-ext: true
|
||||
esbuild: true
|
||||
highlight.js: true
|
||||
prisma: true
|
||||
tesseract.js: true
|
||||
uglifyjs-webpack-plugin: true
|
||||
|
||||
Reference in New Issue
Block a user