first commit

This commit is contained in:
Mamadou Khoussa [028918 DSI/DAC/DIF/DS] 2025-10-30 03:43:36 +00:00
commit f249567baa
38 changed files with 14039 additions and 0 deletions

58
.gitignore vendored Normal file
View File

@ -0,0 +1,58 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
/generated/prisma

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

76
Dockerfile Normal file
View File

@ -0,0 +1,76 @@
# Stage 1: Build
FROM node:20-alpine AS builder
# Définir le répertoire de travail
WORKDIR /app
# Copier les fichiers de dépendances
COPY package*.json ./
# Copier le schema Prisma AVANT npm ci
COPY prisma ./prisma/
# Installer les dépendances avec --legacy-peer-deps pour résoudre les conflits
RUN npm ci --legacy-peer-deps
# Copier le code source
COPY . .
# Générer Prisma Client
RUN npx prisma generate
# Builder l'application
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
# Installer dumb-init pour une meilleure gestion des signaux
RUN apk add --no-cache dumb-init
# Créer un utilisateur non-root
RUN addgroup -g 1001 -S nodejs && \
adduser -S nestjs -u 1001
# Définir le répertoire de travail
WORKDIR /app
# Copier package.json et package-lock.json
COPY package*.json ./
# Copier le schema Prisma
COPY prisma ./prisma/
# Installer UNIQUEMENT les dépendances de production avec --legacy-peer-deps
RUN npm ci --omit=dev --legacy-peer-deps && \
npm cache clean --force
# 🔥 IMPORTANT: Générer Prisma Client en production
RUN npx prisma generate
# Copier le code buildé depuis le builder
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist
# 🔥 IMPORTANT: Copier les fichiers générés de Prisma depuis le builder
COPY --from=builder --chown=nestjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder --chown=nestjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma
# Si vous utilisez un output personnalisé dans schema.prisma, copiez aussi:
# COPY --from=builder --chown=nestjs:nodejs /app/generated ./generated
# Changer le propriétaire des fichiers
RUN chown -R nestjs:nodejs /app
# Utiliser l'utilisateur non-root
USER nestjs
# Exposer le port
EXPOSE 3000
# Healthcheck
#HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
# CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Démarrer l'application avec dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/main"]

103
README.md Normal file
View File

@ -0,0 +1,103 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
## docker
docker build -t service-core:latest .
docker run -p 3000:3000 service-core:latest

43
eslint.config.mjs Normal file
View File

@ -0,0 +1,43 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import { off } from 'process';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'module', // Changé de 'commonjs' à 'module'
parserOptions: {
project: './tsconfig.json', // Ajout du chemin vers tsconfig
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment':'off',
'eslint-disable-next-line @typescript-eslint/no-unsafe-call':'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'prettier/prettier': ['error', {
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
tabWidth: 2,
}],
},
},
);

8
nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

11728
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

88
package.json Normal file
View File

@ -0,0 +1,88 @@
{
"name": "merchant-config",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/axios": "^4.0.1",
"@nestjs/bull": "^11.0.4",
"@nestjs/cache-manager": "^3.0.1",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "^11.0.1",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.0.1",
"@nestjs/swagger": "^11.2.1",
"@prisma/client": "^6.17.1",
"bcrypt": "^6.0.0",
"cache-manager-redis-store": "^3.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"passport-headerapikey": "^1.2.2",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"prisma": "^6.17.1",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

8
prisma.md Normal file
View File

@ -0,0 +1,8 @@
# Créer les migrations
npx prisma migrate dev --name init
# Générer le client Prisma
npx prisma generate
# Format
npx prisma format

View File

@ -0,0 +1,185 @@
-- CreateEnum
CREATE TYPE "OperatorCode" AS ENUM ('ORANGE', 'MTN', 'AIRTEL', 'VODACOM', 'MOOV');
-- CreateEnum
CREATE TYPE "PaymentStatus" AS ENUM ('PENDING', 'SUCCESS', 'FAILED', 'REFUNDED');
-- CreateEnum
CREATE TYPE "SubscriptionStatus" AS ENUM ('PENDING', 'TRIAL', 'ACTIVE', 'SUSPENDED', 'CANCELLED', 'EXPIRED', 'FAILED');
-- CreateTable
CREATE TABLE "Partner" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"apiKey" TEXT NOT NULL,
"secretKey" TEXT NOT NULL,
"status" TEXT NOT NULL,
"callbacks" JSONB,
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Partner_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Operator" (
"id" TEXT NOT NULL,
"code" "OperatorCode" NOT NULL,
"name" TEXT NOT NULL,
"country" TEXT NOT NULL,
"config" JSONB NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Operator_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"msisdn" TEXT NOT NULL,
"userToken" TEXT NOT NULL,
"userAlias" TEXT NOT NULL,
"operatorId" TEXT NOT NULL,
"partnerId" TEXT NOT NULL,
"country" TEXT NOT NULL,
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Plan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"amount" DOUBLE PRECISION NOT NULL,
"currency" TEXT NOT NULL,
"interval" TEXT NOT NULL,
"metadata" JSONB,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Plan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Subscription" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"planId" TEXT NOT NULL,
"partnerId" TEXT NOT NULL,
"status" "SubscriptionStatus" NOT NULL,
"currentPeriodStart" TIMESTAMP(3) NOT NULL,
"currentPeriodEnd" TIMESTAMP(3) NOT NULL,
"nextBillingDate" TIMESTAMP(3),
"trialEndsAt" TIMESTAMP(3),
"cancelledAt" TIMESTAMP(3),
"suspendedAt" TIMESTAMP(3),
"failureCount" INTEGER NOT NULL DEFAULT 0,
"renewalCount" INTEGER NOT NULL DEFAULT 0,
"lastPaymentId" TEXT,
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Payment" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"partnerId" TEXT NOT NULL,
"subscriptionId" TEXT,
"amount" DOUBLE PRECISION NOT NULL,
"currency" TEXT NOT NULL,
"description" TEXT NOT NULL,
"reference" TEXT NOT NULL,
"operatorReference" TEXT,
"status" "PaymentStatus" NOT NULL,
"failureReason" TEXT,
"metadata" JSONB,
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Payment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Refund" (
"id" TEXT NOT NULL,
"paymentId" TEXT NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"reason" TEXT,
"status" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Refund_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Webhook" (
"id" TEXT NOT NULL,
"url" TEXT NOT NULL,
"event" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"response" JSONB,
"status" TEXT NOT NULL,
"attempts" INTEGER NOT NULL DEFAULT 0,
"lastAttempt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Partner_email_key" ON "Partner"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Partner_apiKey_key" ON "Partner"("apiKey");
-- CreateIndex
CREATE UNIQUE INDEX "User_msisdn_key" ON "User"("msisdn");
-- CreateIndex
CREATE UNIQUE INDEX "User_userToken_key" ON "User"("userToken");
-- CreateIndex
CREATE UNIQUE INDEX "Payment_reference_key" ON "Payment"("reference");
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_operatorId_fkey" FOREIGN KEY ("operatorId") REFERENCES "Operator"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_planId_fkey" FOREIGN KEY ("planId") REFERENCES "Plan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "Subscription"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_paymentId_fkey" FOREIGN KEY ("paymentId") REFERENCES "Payment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,38 @@
/*
Warnings:
- Added the required column `country` to the `Partner` table without a default value. This is not possible if the table is not empty.
- Added the required column `passwordHash` to the `Partner` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Partner" ADD COLUMN "companyInfo" JSONB,
ADD COLUMN "country" TEXT NOT NULL,
ADD COLUMN "keysRotatedAt" TIMESTAMP(3),
ADD COLUMN "passwordHash" TEXT NOT NULL,
ALTER COLUMN "status" SET DEFAULT 'PENDING';
-- CreateTable
CREATE TABLE "AuthSession" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"partnerId" TEXT NOT NULL,
"userId" TEXT,
"msisdn" TEXT NOT NULL,
"operator" TEXT NOT NULL,
"country" TEXT NOT NULL,
"authMethod" TEXT NOT NULL,
"challengeId" TEXT,
"status" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AuthSession_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "AuthSession_sessionId_key" ON "AuthSession"("sessionId");
-- AddForeignKey
ALTER TABLE "AuthSession" ADD CONSTRAINT "AuthSession_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,110 @@
/*
Warnings:
- A unique constraint covering the columns `[partnerId,code]` on the table `Plan` will be added. If there are existing duplicate values, this will fail.
- Added the required column `code` to the `Plan` table without a default value. This is not possible if the table is not empty.
- Added the required column `partnerId` to the `Plan` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Plan" ADD COLUMN "code" TEXT NOT NULL,
ADD COLUMN "features" JSONB,
ADD COLUMN "intervalCount" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "limits" JSONB,
ADD COLUMN "partnerId" TEXT NOT NULL,
ADD COLUMN "trialDays" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "Webhook" ADD COLUMN "partnerId" TEXT;
-- CreateTable
CREATE TABLE "Invoice" (
"id" TEXT NOT NULL,
"number" TEXT NOT NULL,
"subscriptionId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"partnerId" TEXT NOT NULL,
"paymentId" TEXT,
"amount" DOUBLE PRECISION NOT NULL,
"currency" TEXT NOT NULL,
"status" TEXT NOT NULL,
"billingPeriodStart" TIMESTAMP(3) NOT NULL,
"billingPeriodEnd" TIMESTAMP(3) NOT NULL,
"dueDate" TIMESTAMP(3) NOT NULL,
"paidAt" TIMESTAMP(3),
"items" JSONB NOT NULL,
"attempts" INTEGER NOT NULL DEFAULT 0,
"failureReason" TEXT,
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Invoice_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Notification" (
"id" TEXT NOT NULL,
"partnerId" TEXT NOT NULL,
"userId" TEXT,
"type" TEXT NOT NULL,
"channel" TEXT NOT NULL,
"recipient" TEXT NOT NULL,
"subject" TEXT,
"content" TEXT NOT NULL,
"templateId" TEXT,
"status" TEXT NOT NULL,
"batchId" TEXT,
"scheduledFor" TIMESTAMP(3),
"sentAt" TIMESTAMP(3),
"failedAt" TIMESTAMP(3),
"failureReason" TEXT,
"response" JSONB,
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Notification_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Invoice_number_key" ON "Invoice"("number");
-- CreateIndex
CREATE UNIQUE INDEX "Invoice_paymentId_key" ON "Invoice"("paymentId");
-- CreateIndex
CREATE INDEX "Invoice_subscriptionId_idx" ON "Invoice"("subscriptionId");
-- CreateIndex
CREATE INDEX "Invoice_partnerId_status_idx" ON "Invoice"("partnerId", "status");
-- CreateIndex
CREATE INDEX "Plan_partnerId_active_idx" ON "Plan"("partnerId", "active");
-- CreateIndex
CREATE UNIQUE INDEX "Plan_partnerId_code_key" ON "Plan"("partnerId", "code");
-- AddForeignKey
ALTER TABLE "Plan" ADD CONSTRAINT "Plan_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "Subscription"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_paymentId_fkey" FOREIGN KEY ("paymentId") REFERENCES "Payment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,211 @@
/*
Warnings:
- You are about to drop the `AuthSession` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Invoice` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Notification` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Operator` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Partner` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Payment` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Plan` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Refund` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Subscription` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Webhook` table. If the table is not empty, all the data it contains will be lost.
*/
-- CreateEnum
CREATE TYPE "Currency" AS ENUM ('XOF', 'XAF', 'EURO', 'DOLLARS');
-- CreateEnum
CREATE TYPE "Periodicity" AS ENUM ('Daily', 'Weekly', 'Monthly', 'OneTime');
-- DropForeignKey
ALTER TABLE "public"."AuthSession" DROP CONSTRAINT "AuthSession_partnerId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Invoice" DROP CONSTRAINT "Invoice_partnerId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Invoice" DROP CONSTRAINT "Invoice_paymentId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Invoice" DROP CONSTRAINT "Invoice_subscriptionId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Invoice" DROP CONSTRAINT "Invoice_userId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Notification" DROP CONSTRAINT "Notification_partnerId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Notification" DROP CONSTRAINT "Notification_userId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Payment" DROP CONSTRAINT "Payment_partnerId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Payment" DROP CONSTRAINT "Payment_subscriptionId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Payment" DROP CONSTRAINT "Payment_userId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Plan" DROP CONSTRAINT "Plan_partnerId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Refund" DROP CONSTRAINT "Refund_paymentId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Subscription" DROP CONSTRAINT "Subscription_partnerId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Subscription" DROP CONSTRAINT "Subscription_planId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Subscription" DROP CONSTRAINT "Subscription_userId_fkey";
-- DropForeignKey
ALTER TABLE "public"."User" DROP CONSTRAINT "User_operatorId_fkey";
-- DropForeignKey
ALTER TABLE "public"."User" DROP CONSTRAINT "User_partnerId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Webhook" DROP CONSTRAINT "Webhook_partnerId_fkey";
-- DropTable
DROP TABLE "public"."AuthSession";
-- DropTable
DROP TABLE "public"."Invoice";
-- DropTable
DROP TABLE "public"."Notification";
-- DropTable
DROP TABLE "public"."Operator";
-- DropTable
DROP TABLE "public"."Partner";
-- DropTable
DROP TABLE "public"."Payment";
-- DropTable
DROP TABLE "public"."Plan";
-- DropTable
DROP TABLE "public"."Refund";
-- DropTable
DROP TABLE "public"."Subscription";
-- DropTable
DROP TABLE "public"."User";
-- DropTable
DROP TABLE "public"."Webhook";
-- DropEnum
DROP TYPE "public"."OperatorCode";
-- DropEnum
DROP TYPE "public"."PaymentStatus";
-- DropEnum
DROP TYPE "public"."SubscriptionStatus";
-- CreateTable
CREATE TABLE "merchant_partners" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"logo" TEXT,
"description" TEXT,
"adresse" TEXT,
"phone" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "merchant_partners_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "configs" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"merchantPartnerId" INTEGER NOT NULL,
"operatorId" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "configs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "services" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"merchantPartnerId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "services_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "pricings" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"type" "Periodicity" NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"tax" DOUBLE PRECISION NOT NULL,
"currency" "Currency" NOT NULL,
"periodicity" "Periodicity" NOT NULL,
"serviceId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "pricings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "technical_contacts" (
"id" SERIAL NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"email" TEXT NOT NULL,
"merchantPartnerId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "technical_contacts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "operators" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "operators_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "configs" ADD CONSTRAINT "configs_merchantPartnerId_fkey" FOREIGN KEY ("merchantPartnerId") REFERENCES "merchant_partners"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "configs" ADD CONSTRAINT "configs_operatorId_fkey" FOREIGN KEY ("operatorId") REFERENCES "operators"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "services" ADD CONSTRAINT "services_merchantPartnerId_fkey" FOREIGN KEY ("merchantPartnerId") REFERENCES "merchant_partners"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "pricings" ADD CONSTRAINT "pricings_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "services"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "technical_contacts" ADD CONSTRAINT "technical_contacts_merchantPartnerId_fkey" FOREIGN KEY ("merchantPartnerId") REFERENCES "merchant_partners"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,20 @@
-- CreateEnum
CREATE TYPE "MerchantUserRole" AS ENUM ('ADMIN', 'MANAGER', 'TECHNICAL', 'VIEWER');
-- CreateTable
CREATE TABLE "merchant_users" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"merchantPartnerId" INTEGER NOT NULL,
"role" "MerchantUserRole" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "merchant_users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "merchant_users_userId_merchantPartnerId_key" ON "merchant_users"("userId", "merchantPartnerId");
-- AddForeignKey
ALTER TABLE "merchant_users" ADD CONSTRAINT "merchant_users_merchantPartnerId_fkey" FOREIGN KEY ("merchantPartnerId") REFERENCES "merchant_partners"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

140
prisma/schema.prisma Normal file
View File

@ -0,0 +1,140 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum MerchantUserRole {
ADMIN
MANAGER
TECHNICAL
VIEWER
}
enum Currency {
XOF
XAF
EURO
DOLLARS
}
enum Periodicity {
Daily
Weekly
Monthly
OneTime
}
model MerchantUser {
id Int @id @default(autoincrement())
userId String // ID from external user service
merchantPartnerId Int
role MerchantUserRole
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
merchantPartner MerchantPartner @relation(fields: [merchantPartnerId], references: [id], onDelete: Cascade)
@@unique([userId, merchantPartnerId])
@@map("merchant_users")
}
model MerchantPartner {
id Int @id @default(autoincrement())
name String
logo String?
description String?
adresse String?
phone String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
configs Config[]
services Service[]
technicalContacts TechnicalContact[]
merchantUsers MerchantUser[]
@@map("merchant_partners")
}
model Config {
id Int @id @default(autoincrement())
name String
value String
merchantPartnerId Int
operatorId Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
merchantPartner MerchantPartner @relation(fields: [merchantPartnerId], references: [id], onDelete: Cascade)
operator Operator? @relation(fields: [operatorId], references: [id])
@@map("configs")
}
model Service {
id Int @id @default(autoincrement())
name String
description String?
merchantPartnerId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
merchantPartner MerchantPartner @relation(fields: [merchantPartnerId], references: [id], onDelete: Cascade)
plans Plan[]
@@map("services")
}
model Plan {
id Int @id @default(autoincrement())
name String
type Periodicity
amount Float
tax Float
currency Currency
periodicity Periodicity
serviceId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
@@map("pricings")
}
model TechnicalContact {
id Int @id @default(autoincrement())
firstName String
lastName String
phone String
email String
merchantPartnerId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
merchantPartner MerchantPartner @relation(fields: [merchantPartnerId], references: [id], onDelete: Cascade)
@@map("technical_contacts")
}
model Operator {
id Int @id @default(autoincrement())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
configs Config[]
@@map("operators")
}

11
prisma/test.prisma Normal file
View File

@ -0,0 +1,11 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // or "mysql", "sqlite", etc.
url = env("DATABASE_URL")
}

View File

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

12
src/app.controller.ts Normal file
View File

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

41
src/app.module.ts Normal file
View File

@ -0,0 +1,41 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { BullModule } from '@nestjs/bull';
import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { CacheModule } from '@nestjs/cache-manager';
import * as redisStore from 'cache-manager-redis-store';
// Import des configurations
// Import des modules
import { PrismaService } from './shared/services/prisma.service';
import { MerchantModule } from './merchant/merchant.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
//load: [],
envFilePath: ['.env.local', '.env'],
}),
CacheModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
store: redisStore,
host: configService.get('app.redis.host'),
port: configService.get('app.redis.port'),
password: configService.get('app.redis.password'),
ttl: 600, // 10 minutes default
}),
inject: [ConfigService],
isGlobal: true,
}),
MerchantModule
],
providers: [PrismaService],
exports: [PrismaService],
})
export class AppModule {}

8
src/app.service.ts Normal file
View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

50
src/main.ts Normal file
View File

@ -0,0 +1,50 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global prefix
app.setGlobalPrefix('api/v1');
// Validation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
// CORS
app.enableCors({
origin: process.env.CORS_ORIGINS?.split(',') || '*',
credentials: true,
});
// Swagger
const config = new DocumentBuilder()
.setTitle('Payment Hub API')
.setDescription('Unified DCB Payment Aggregation Platform')
.setVersion('1.0.0')
.addBearerAuth()
.addTag('payments')
.addTag('subscriptions')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);
app.getHttpAdapter().get('/api/swagger-json', (req, res) => {
res.json(document);
});
const port = process.env.PORT || 3000;
await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`);
console.log(`Swagger docs: http://localhost:${port}/api/docs`);
console.log(`Swagger docs: http://localhost:${port}/api/swagger-json`);
}
bootstrap();

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateMerchantPartnerDto } from './create.merchant.dto';
export class UpdateMerchantPartnerDto extends PartialType(CreateMerchantPartnerDto) {}

View File

@ -0,0 +1,119 @@
import { IsString, IsOptional, IsArray, ValidateNested, IsEnum, IsNotEmpty } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export enum MerchantUserRole {
ADMIN = 'ADMIN',
MANAGER = 'MANAGER',
TECHNICAL = 'TECHNICAL',
VIEWER = 'VIEWER',
}
export class CreateConfigDto {
@ApiProperty({ description: 'Configuration name' })
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({ description: 'Configuration value' })
@IsString()
@IsNotEmpty()
value: string;
@ApiPropertyOptional({ description: 'Operator ID (optional)' })
@IsOptional()
operatorId?: number;
}
export class CreateMerchantUserDto {
@ApiProperty({ description: 'User ID from external user service' })
@IsString()
@IsNotEmpty()
userId: string;
@ApiProperty({
description: 'User role in merchant context',
enum: MerchantUserRole
})
@IsEnum(MerchantUserRole)
role: MerchantUserRole;
}
export class CreateTechnicalContactDto {
@ApiProperty({ description: 'First name' })
@IsString()
@IsNotEmpty()
firstName: string;
@ApiProperty({ description: 'Last name' })
@IsString()
@IsNotEmpty()
lastName: string;
@ApiProperty({ description: 'Phone number' })
@IsString()
@IsNotEmpty()
phone: string;
@ApiProperty({ description: 'Email address' })
@IsString()
@IsNotEmpty()
email: string;
}
export class CreateMerchantPartnerDto {
@ApiProperty({ description: 'Merchant name' })
@IsString()
@IsNotEmpty()
name: string;
@ApiPropertyOptional({ description: 'Logo URL' })
@IsString()
@IsOptional()
logo?: string;
@ApiPropertyOptional({ description: 'Merchant description' })
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({ description: 'Address' })
@IsString()
@IsOptional()
adresse?: string;
@ApiPropertyOptional({ description: 'Phone number' })
@IsString()
@IsOptional()
phone?: string;
@ApiPropertyOptional({
description: 'Initial configurations',
type: [CreateConfigDto]
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateConfigDto)
@IsOptional()
configs?: CreateConfigDto[];
@ApiPropertyOptional({
description: 'Users to attach to merchant',
type: [CreateMerchantUserDto]
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateMerchantUserDto)
@IsOptional()
users?: CreateMerchantUserDto[];
@ApiPropertyOptional({
description: 'Technical contacts',
type: [CreateTechnicalContactDto]
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateTechnicalContactDto)
@IsOptional()
technicalContacts?: CreateTechnicalContactDto[];
}

View File

@ -0,0 +1,31 @@
import { IsString, IsEnum, IsNotEmpty, IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { MerchantUserRole } from './create.merchant.dto';
export class AddUserToMerchantDto {
@ApiProperty({ description: 'User ID from external user service' })
@IsString()
@IsNotEmpty()
userId: string;
@ApiProperty({
description: 'User role in merchant context',
enum: MerchantUserRole
})
@IsEnum(MerchantUserRole)
role: MerchantUserRole;
@ApiProperty({ description: 'Merchant Partner ID' })
@IsNumber()
@IsNotEmpty()
merchantPartnerId: number;
}
export class UpdateUserRoleDto {
@ApiProperty({
description: 'New user role',
enum: MerchantUserRole
})
@IsEnum(MerchantUserRole)
role: MerchantUserRole;
}

View File

@ -0,0 +1,32 @@
import { MerchantPartner, Config, MerchantUser, TechnicalContact } from "generated/prisma";
export type MerchantPartnerWithRelations = MerchantPartner & {
configs?: Config[];
merchantUsers?: MerchantUserWithInfo[];
technicalContacts?: TechnicalContact[];
};
export type MerchantUserWithInfo = MerchantUser & {
userInfo?: {
email?: string;
name?: string;
firstName?: string;
lastName?: string;
};
};
export interface MerchantCreatedEvent {
merchantId: number;
merchantName: string;
createdBy?: string;
timestamp: Date;
}
export interface UserAttachedToMerchantEvent {
merchantId: number;
userId: string;
role: string;
timestamp: Date;
}

View File

@ -0,0 +1,31 @@
export interface UserServiceClient {
/**
* Verify if a user exists in the external user service
* @param userId - The user ID to verify
* @returns Promise<boolean> - True if user exists
*/
verifyUserExists(userId: string): Promise<boolean>;
/**
* Get user information from external service
* @param userId - The user ID
* @returns Promise<UserInfo> - User information
*/
getUserInfo(userId: string): Promise<UserInfo>;
/**
* Get multiple users information
* @param userIds - Array of user IDs
* @returns Promise<UserInfo[]> - Array of user information
*/
getUsersInfo(userIds: string[]): Promise<UserInfo[]>;
}
export interface UserInfo {
id: string;
email?: string;
name?: string;
firstName?: string;
lastName?: string;
[key: string]: any;
}

View File

@ -0,0 +1,166 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
ParseIntPipe,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
import { MerchantService } from './services/merchant.service';
import { CreateMerchantPartnerDto } from './dto/create.merchant.dto';
import { UpdateMerchantPartnerDto } from './dto/ update.merchant.dto';
import { AddUserToMerchantDto, UpdateUserRoleDto } from './dto/merchant.user.dto';
@ApiTags('merchants')
@Controller('merchants')
export class MerchantController {
constructor(private readonly merchantService: MerchantService) {}
@Post()
@ApiOperation({ summary: 'Create a new merchant partner with configs and users' })
@ApiResponse({ status: 201, description: 'Merchant created successfully' })
@ApiResponse({ status: 400, description: 'Bad request - validation failed' })
create(@Body() createMerchantDto: CreateMerchantPartnerDto) {
return this.merchantService.createMerchant(createMerchantDto);
}
@Get()
@ApiOperation({ summary: 'Get all merchants' })
@ApiQuery({ name: 'skip', required: false, type: Number })
@ApiQuery({ name: 'take', required: false, type: Number })
@ApiResponse({ status: 200, description: 'List of merchants' })
findAll(
@Query('skip') skip?: string,
@Query('take') take?: string,
) {
return this.merchantService.findAll(
skip ? parseInt(skip) : 0,
take ? parseInt(take) : 10,
);
}
@Get(':id')
@ApiOperation({ summary: 'Get merchant by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiResponse({ status: 200, description: 'Merchant found' })
@ApiResponse({ status: 404, description: 'Merchant not found' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.merchantService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update merchant' })
@ApiParam({ name: 'id', type: Number })
@ApiResponse({ status: 200, description: 'Merchant updated successfully' })
@ApiResponse({ status: 404, description: 'Merchant not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateMerchantDto: UpdateMerchantPartnerDto,
) {
return this.merchantService.update(id, updateMerchantDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete merchant' })
@ApiParam({ name: 'id', type: Number })
@ApiResponse({ status: 204, description: 'Merchant deleted successfully' })
@ApiResponse({ status: 404, description: 'Merchant not found' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.merchantService.remove(id);
}
// User Management Endpoints
@Post('users')
@ApiOperation({ summary: 'Add user to merchant' })
@ApiResponse({ status: 201, description: 'User added to merchant successfully' })
@ApiResponse({ status: 400, description: 'Bad request' })
@ApiResponse({ status: 409, description: 'User already attached to merchant' })
addUserToMerchant(@Body() dto: AddUserToMerchantDto) {
return this.merchantService.addUserToMerchant(dto);
}
@Get(':id/users')
@ApiOperation({ summary: 'Get all users of a merchant' })
@ApiParam({ name: 'id', type: Number })
@ApiResponse({ status: 200, description: 'List of merchant users' })
getMerchantUsers(@Param('id', ParseIntPipe) id: number) {
return this.merchantService.getMerchantUsers(id);
}
@Patch(':merchantId/users/:userId/role')
@ApiOperation({ summary: 'Update user role in merchant' })
@ApiParam({ name: 'merchantId', type: Number })
@ApiParam({ name: 'userId', type: String })
@ApiResponse({ status: 200, description: 'User role updated successfully' })
@ApiResponse({ status: 404, description: 'User or merchant not found' })
updateUserRole(
@Param('merchantId', ParseIntPipe) merchantId: number,
@Param('userId') userId: string,
@Body() dto: UpdateUserRoleDto,
) {
return this.merchantService.updateUserRole(merchantId, userId, dto);
}
@Delete(':merchantId/users/:userId')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Remove user from merchant' })
@ApiParam({ name: 'merchantId', type: Number })
@ApiParam({ name: 'userId', type: String })
@ApiResponse({ status: 204, description: 'User removed successfully' })
@ApiResponse({ status: 404, description: 'User or merchant not found' })
removeUserFromMerchant(
@Param('merchantId', ParseIntPipe) merchantId: number,
@Param('userId') userId: string,
) {
return this.merchantService.removeUserFromMerchant(merchantId, userId);
}
@Get('user/:userId')
@ApiOperation({ summary: 'Get all merchants for a specific user' })
@ApiParam({ name: 'userId', type: String })
@ApiResponse({ status: 200, description: 'List of user merchants' })
getUserMerchants(@Param('userId') userId: string) {
return this.merchantService.getUserMerchants(userId);
}
// Config Management Endpoints
@Post(':id/configs')
@ApiOperation({ summary: 'Add configuration to merchant' })
@ApiParam({ name: 'id', type: Number })
@ApiResponse({ status: 201, description: 'Configuration added successfully' })
addConfig(
@Param('id', ParseIntPipe) id: number,
@Body() body: { name: string; value: string; operatorId?: number },
) {
return this.merchantService.addConfig(id, body.name, body.value, body.operatorId);
}
@Patch('configs/:configId')
@ApiOperation({ summary: 'Update configuration value' })
@ApiParam({ name: 'configId', type: Number })
@ApiResponse({ status: 200, description: 'Configuration updated successfully' })
updateConfig(
@Param('configId', ParseIntPipe) configId: number,
@Body() body: { value: string },
) {
return this.merchantService.updateConfig(configId, body.value);
}
@Delete('configs/:configId')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete configuration' })
@ApiParam({ name: 'configId', type: Number })
@ApiResponse({ status: 204, description: 'Configuration deleted successfully' })
deleteConfig(@Param('configId', ParseIntPipe) configId: number) {
return this.merchantService.deleteConfig(configId);
}
}

View File

@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { ConfigModule } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { MerchantController } from './merchant.controller';
import { HttpUserServiceClient } from './services/user.service.client';
import { PrismaService } from 'src/shared/services/prisma.service';
import { MerchantService } from './services/merchant.service';
@Module({
imports: [
HttpModule,
ConfigModule,
EventEmitterModule.forRoot(),
],
controllers: [MerchantController],
providers: [
MerchantService,
PrismaService,
HttpUserServiceClient
],
exports: [MerchantService],
})
export class MerchantModule {}

View File

@ -0,0 +1,413 @@
import { Injectable, NotFoundException, BadRequestException, ConflictException, Inject } from '@nestjs/common';
import { MerchantPartnerWithRelations, MerchantUserWithInfo } from '../entities/merchant.entity';
import { EventEmitter2 } from '@nestjs/event-emitter';
import type { UserServiceClient } from '../interfaces/ user.service.interface';
import { CreateMerchantPartnerDto } from '../dto/create.merchant.dto';
import { UpdateMerchantPartnerDto } from '../dto/ update.merchant.dto';
import { AddUserToMerchantDto, UpdateUserRoleDto } from '../dto/merchant.user.dto';
import { PrismaService } from 'src/shared/services/prisma.service';
@Injectable()
export class MerchantService {
constructor(
private readonly prisma: PrismaService,
@Inject()
private readonly userServiceClient: UserServiceClient,
private readonly eventEmitter: EventEmitter2,
) {}
/**
* Create a new merchant partner with configs and users
*/
async createMerchant(dto: CreateMerchantPartnerDto): Promise<MerchantPartnerWithRelations> {
// Validate users exist in external service
if (dto.users && dto.users.length > 0) {
await this.validateUsers(dto.users.map(u => u.userId));
}
// Create merchant with all related data in a transaction
const merchant = await this.prisma.$transaction(async (prisma) => {
// Create merchant
const createdMerchant = await prisma.merchantPartner.create({
data: {
name: dto.name,
logo: dto.logo,
description: dto.description,
adresse: dto.adresse,
phone: dto.phone,
},
});
// Create configs if provided
if (dto.configs && dto.configs.length > 0) {
await prisma.config.createMany({
data: dto.configs.map(config => ({
name: config.name,
value: config.value,
operatorId: config.operatorId,
merchantPartnerId: createdMerchant.id,
})),
});
}
// Create merchant users if provided
if (dto.users && dto.users.length > 0) {
await prisma.merchantUser.createMany({
data: dto.users.map(user => ({
userId: user.userId,
role: user.role,
merchantPartnerId: createdMerchant.id,
})),
});
}
// Create technical contacts if provided
if (dto.technicalContacts && dto.technicalContacts.length > 0) {
await prisma.technicalContact.createMany({
data: dto.technicalContacts.map(contact => ({
firstName: contact.firstName,
lastName: contact.lastName,
phone: contact.phone,
email: contact.email,
merchantPartnerId: createdMerchant.id,
})),
});
}
// Fetch complete merchant with relations
return prisma.merchantPartner.findUnique({
where: { id: createdMerchant.id },
include: {
configs: true,
merchantUsers: true,
technicalContacts: true,
},
});
});
// Emit event
this.eventEmitter.emit('merchant.created', {
merchantId: merchant?.id,
merchantName: merchant?.name,
timestamp: new Date(),
});
// Enrich with user info
return this.enrichMerchantWithUserInfo(merchant);
}
/**
* Find all merchants with optional pagination
*/
async findAll(skip = 0, take = 10): Promise<MerchantPartnerWithRelations[]> {
const merchants = await this.prisma.merchantPartner.findMany({
skip,
take,
include: {
configs: true,
merchantUsers: true,
technicalContacts: true,
},
orderBy: {
createdAt: 'desc',
},
});
return Promise.all(merchants.map(m => this.enrichMerchantWithUserInfo(m)));
}
/**
* Find merchant by ID
*/
async findOne(id: number): Promise<MerchantPartnerWithRelations> {
const merchant = await this.prisma.merchantPartner.findUnique({
where: { id },
include: {
configs: true,
merchantUsers: true,
technicalContacts: true,
services: {
include: {
plans: true,
},
},
},
});
if (!merchant) {
throw new NotFoundException(`Merchant with ID ${id} not found`);
}
return this.enrichMerchantWithUserInfo(merchant);
}
/**
* Update merchant
*/
async update(id: number, dto: UpdateMerchantPartnerDto): Promise<MerchantPartnerWithRelations> {
await this.findOne(id); // Check if exists
const merchant = await this.prisma.merchantPartner.update({
where: { id },
data: {
name: dto.name,
logo: dto.logo,
description: dto.description,
adresse: dto.adresse,
phone: dto.phone,
},
include: {
configs: true,
merchantUsers: true,
technicalContacts: true,
},
});
return this.enrichMerchantWithUserInfo(merchant);
}
/**
* Delete merchant (soft delete could be implemented)
*/
async remove(id: number): Promise<void> {
await this.findOne(id); // Check if exists
await this.prisma.merchantPartner.delete({
where: { id },
});
this.eventEmitter.emit('merchant.deleted', {
merchantId: id,
timestamp: new Date(),
});
}
/**
* Add user to merchant
*/
async addUserToMerchant(dto: AddUserToMerchantDto): Promise<MerchantUserWithInfo> {
// Check if merchant exists
await this.findOne(dto.merchantPartnerId);
// Validate user exists
const userExists = await this.userServiceClient.verifyUserExists(dto.userId);
if (!userExists) {
throw new BadRequestException(`User with ID ${dto.userId} not found in user service`);
}
// Check if user already attached
const existing = await this.prisma.merchantUser.findUnique({
where: {
userId_merchantPartnerId: {
userId: dto.userId,
merchantPartnerId: dto.merchantPartnerId,
},
},
});
if (existing) {
throw new ConflictException(`User ${dto.userId} is already attached to merchant ${dto.merchantPartnerId}`);
}
// Create merchant user
const merchantUser = await this.prisma.merchantUser.create({
data: {
userId: dto.userId,
role: dto.role,
merchantPartnerId: dto.merchantPartnerId,
},
});
// Emit event
this.eventEmitter.emit('merchant.user.attached', {
merchantId: dto.merchantPartnerId,
userId: dto.userId,
role: dto.role,
timestamp: new Date(),
});
// Enrich with user info
const userInfo = await this.userServiceClient.getUserInfo(dto.userId);
return {
...merchantUser,
userInfo,
};
}
/**
* Remove user from merchant
*/
async removeUserFromMerchant(merchantId: number, userId: string): Promise<void> {
const merchantUser = await this.prisma.merchantUser.findUnique({
where: {
userId_merchantPartnerId: {
userId,
merchantPartnerId: merchantId,
},
},
});
if (!merchantUser) {
throw new NotFoundException(`User ${userId} not found in merchant ${merchantId}`);
}
await this.prisma.merchantUser.delete({
where: {
id: merchantUser.id,
},
});
this.eventEmitter.emit('merchant.user.removed', {
merchantId,
userId,
timestamp: new Date(),
});
}
/**
* Update user role in merchant
*/
async updateUserRole(merchantId: number, userId: string, dto: UpdateUserRoleDto): Promise<MerchantUserWithInfo> {
const merchantUser = await this.prisma.merchantUser.findUnique({
where: {
userId_merchantPartnerId: {
userId,
merchantPartnerId: merchantId,
},
},
});
if (!merchantUser) {
throw new NotFoundException(`User ${userId} not found in merchant ${merchantId}`);
}
const updated = await this.prisma.merchantUser.update({
where: { id: merchantUser.id },
data: { role: dto.role },
});
this.eventEmitter.emit('merchant.user.role.updated', {
merchantId,
userId,
oldRole: merchantUser.role,
newRole: dto.role,
timestamp: new Date(),
});
// Enrich with user info
const userInfo = await this.userServiceClient.getUserInfo(userId);
return {
...updated,
userInfo,
};
}
/**
* Get all users of a merchant
*/
async getMerchantUsers(merchantId: number): Promise<MerchantUserWithInfo[]> {
await this.findOne(merchantId); // Check if exists
const merchantUsers = await this.prisma.merchantUser.findMany({
where: { merchantPartnerId: merchantId },
});
// Enrich with user info
const userIds = merchantUsers.map(mu => mu.userId);
const usersInfo = await this.userServiceClient.getUsersInfo(userIds);
return merchantUsers.map(mu => ({
...mu,
userInfo: usersInfo.find(u => u.id === mu.userId),
}));
}
/**
* Get merchants for a specific user
*/
async getUserMerchants(userId: string): Promise<MerchantPartnerWithRelations[]> {
const merchantUsers = await this.prisma.merchantUser.findMany({
where: { userId },
include: {
merchantPartner: {
include: {
configs: true,
technicalContacts: true,
},
},
},
});
return merchantUsers.map(mu => mu.merchantPartner);
}
/**
* Add configuration to merchant
*/
async addConfig(merchantId: number, name: string, value: string, operatorId?: number) {
await this.findOne(merchantId); // Check if exists
return this.prisma.config.create({
data: {
name,
value,
operatorId,
merchantPartnerId: merchantId,
},
});
}
/**
* Update configuration
*/
async updateConfig(configId: number, value: string) {
return this.prisma.config.update({
where: { id: configId },
data: { value },
});
}
/**
* Delete configuration
*/
async deleteConfig(configId: number) {
await this.prisma.config.delete({
where: { id: configId },
});
}
// Private helper methods
private async validateUsers(userIds: string[]): Promise<void> {
const validations = await Promise.all(
userIds.map(id => this.userServiceClient.verifyUserExists(id)),
);
const invalidUsers = userIds.filter((_, index) => !validations[index]);
if (invalidUsers.length > 0) {
throw new BadRequestException(
`The following users were not found: ${invalidUsers.join(', ')}`,
);
}
}
private async enrichMerchantWithUserInfo(
merchant: any,
): Promise<MerchantPartnerWithRelations> {
if (!merchant.merchantUsers || merchant.merchantUsers.length === 0) {
return merchant;
}
const userIds = merchant.merchantUsers.map(mu => mu.userId);
const usersInfo = await this.userServiceClient.getUsersInfo(userIds);
return {
...merchant,
merchantUsers: merchant.merchantUsers.map(mu => ({
...mu,
userInfo: usersInfo.find(u => u.id === mu.userId),
})),
};
}
}

View File

@ -0,0 +1,80 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
import { UserInfo, UserServiceClient } from '../interfaces/ user.service.interface';
@Injectable()
export class HttpUserServiceClient implements UserServiceClient {
private readonly baseUrl: string;
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {
this.baseUrl = this.configService.get<string>('USER_SERVICE_URL') || 'http://localhost:3001';
}
async verifyUserExists(userId: string): Promise<boolean> {
try {
const response = await firstValueFrom(
this.httpService.get(`${this.baseUrl}/users/${userId}/exists`),
);
return response.data.exists === true;
} catch (error) {
if (error.response?.status === 404) {
return false;
}
throw new HttpException(
'Failed to verify user existence',
HttpStatus.SERVICE_UNAVAILABLE,
);
}
}
async getUserInfo(userId: string): Promise<UserInfo> {
try {
const response = await firstValueFrom(
this.httpService.get(`${this.baseUrl}/users/${userId}`),
);
return this.mapToUserInfo(response.data);
} catch (error) {
if (error.response?.status === 404) {
throw new HttpException(`User ${userId} not found`, HttpStatus.NOT_FOUND);
}
throw new HttpException(
'Failed to get user information',
HttpStatus.SERVICE_UNAVAILABLE,
);
}
}
async getUsersInfo(userIds: string[]): Promise<UserInfo[]> {
if (userIds.length === 0) {
return [];
}
try {
const response = await firstValueFrom(
this.httpService.post(`${this.baseUrl}/users/batch`, { userIds }),
);
return response.data.map(user => this.mapToUserInfo(user));
} catch (error) {
throw new HttpException(
'Failed to get users information',
HttpStatus.SERVICE_UNAVAILABLE,
);
}
}
private mapToUserInfo(data: any): UserInfo {
return {
id: data.id || data.userId,
email: data.email,
name: data.name || `${data.firstName} ${data.lastName}`.trim(),
firstName: data.firstName,
lastName: data.lastName,
...data,
};
}
}

View File

@ -0,0 +1,20 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from 'generated/prisma';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
super({
log: ['query', 'info', 'warn', 'error'],
});
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

View File

@ -0,0 +1,38 @@
import * as crypto from 'crypto';
import * as bcrypt from 'bcrypt';
export class CryptoUtil {
static generateRandomString(length: number = 32): string {
return crypto.randomBytes(length).toString('hex');
}
static generateApiKey(): string {
return `pk_${this.generateRandomString(32)}`;
}
static generateSecretKey(): string {
return `sk_${this.generateRandomString(32)}`;
}
static async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
static async comparePassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
static generateSignature(payload: any, secret: string): string {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
return hmac.digest('hex');
}
static verifySignature(payload: any, signature: string, secret: string): boolean {
const expectedSignature = this.generateSignature(payload, secret);
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}
}

View File

@ -0,0 +1,49 @@
export interface PaginationParams {
page?: number;
limit?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
export interface PaginatedResult<T> {
data: T[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
hasPrevious: boolean;
hasNext: boolean;
};
}
export class PaginationUtil {
static getPaginationParams(params: PaginationParams) {
const page = Math.max(1, params.page || 1);
const limit = Math.min(100, Math.max(1, params.limit || 20));
const skip = (page - 1) * limit;
return { page, limit, skip };
}
static createPaginatedResult<T>(
data: T[],
total: number,
params: PaginationParams,
): PaginatedResult<T> {
const { page, limit } = this.getPaginationParams(params);
const totalPages = Math.ceil(total / limit);
return {
data,
meta: {
total,
page,
limit,
totalPages,
hasPrevious: page > 1,
hasNext: page < totalPages,
},
};
}
}

25
test/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
test/jest-e2e.json Normal file
View File

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

25
tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}