first commit
This commit is contained in:
parent
ad91d5c150
commit
bde4f90235
@ -2,6 +2,7 @@
|
|||||||
import eslint from '@eslint/js';
|
import eslint from '@eslint/js';
|
||||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
|
import { off } from 'process';
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
@ -27,6 +28,8 @@ export default tseslint.config(
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@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-floating-promises': 'warn',
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
'prettier/prettier': ['error', {
|
'prettier/prettier': ['error', {
|
||||||
|
|||||||
67
package-lock.json
generated
67
package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/bull": "^11.0.4",
|
"@nestjs/bull": "^11.0.4",
|
||||||
"@nestjs/cache-manager": "^3.0.1",
|
"@nestjs/cache-manager": "^3.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
@ -23,6 +24,7 @@
|
|||||||
"@prisma/client": "^6.17.1",
|
"@prisma/client": "^6.17.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"cache-manager-redis-store": "^3.0.1",
|
"cache-manager-redis-store": "^3.0.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
@ -2192,6 +2194,17 @@
|
|||||||
"@tybys/wasm-util": "^0.10.0"
|
"@tybys/wasm-util": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/axios": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||||
|
"axios": "^1.3.1",
|
||||||
|
"rxjs": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/bull": {
|
"node_modules/@nestjs/bull": {
|
||||||
"version": "11.0.4",
|
"version": "11.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-11.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-11.0.4.tgz",
|
||||||
@ -4461,9 +4474,20 @@
|
|||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||||
|
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
"version": "30.2.0",
|
"version": "30.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
||||||
@ -5031,6 +5055,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/class-transformer": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/class-validator": {
|
"node_modules/class-validator": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
|
||||||
@ -5211,7 +5241,6 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
@ -5524,7 +5553,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
@ -5783,7 +5811,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@ -6428,6 +6455,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@ -6477,7 +6525,6 @@
|
|||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
@ -6494,7 +6541,6 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@ -6504,7 +6550,6 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
@ -6879,7 +6924,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@ -9490,6 +9534,13 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/bull": "^11.0.4",
|
"@nestjs/bull": "^11.0.4",
|
||||||
"@nestjs/cache-manager": "^3.0.1",
|
"@nestjs/cache-manager": "^3.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
@ -34,6 +35,7 @@
|
|||||||
"@prisma/client": "^6.17.1",
|
"@prisma/client": "^6.17.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"cache-manager-redis-store": "^3.0.1",
|
"cache-manager-redis-store": "^3.0.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
|
|||||||
@ -2,4 +2,7 @@
|
|||||||
npx prisma migrate dev --name init
|
npx prisma migrate dev --name init
|
||||||
|
|
||||||
# Générer le client Prisma
|
# Générer le client Prisma
|
||||||
npx prisma generate
|
npx prisma generate
|
||||||
|
|
||||||
|
# Format
|
||||||
|
npx prisma format
|
||||||
@ -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;
|
||||||
@ -45,22 +45,22 @@ model Operator {
|
|||||||
active Boolean @default(true)
|
active Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
users User[]
|
users User[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
msisdn String @unique
|
msisdn String @unique
|
||||||
userToken String @unique
|
userToken String @unique
|
||||||
userAlias String
|
userAlias String
|
||||||
operatorId String
|
operatorId String
|
||||||
partnerId String
|
partnerId String
|
||||||
country String
|
country String
|
||||||
metadata Json?
|
metadata Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
operator Operator @relation(fields: [operatorId], references: [id])
|
operator Operator @relation(fields: [operatorId], references: [id])
|
||||||
partner Partner @relation(fields: [partnerId], references: [id])
|
partner Partner @relation(fields: [partnerId], references: [id])
|
||||||
subscriptions Subscription[]
|
subscriptions Subscription[]
|
||||||
@ -77,11 +77,11 @@ model Plan {
|
|||||||
description String?
|
description String?
|
||||||
amount Float
|
amount Float
|
||||||
currency String
|
currency String
|
||||||
interval String // DAILY, WEEKLY, MONTHLY, YEARLY
|
interval String // DAILY, WEEKLY, MONTHLY, YEARLY
|
||||||
intervalCount Int @default(1)
|
intervalCount Int @default(1)
|
||||||
trialDays Int @default(0)
|
trialDays Int @default(0)
|
||||||
features Json? // Array of features
|
features Json? // Array of features
|
||||||
limits Json? // Object with usage limits
|
limits Json? // Object with usage limits
|
||||||
metadata Json?
|
metadata Json?
|
||||||
active Boolean @default(true)
|
active Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -103,12 +103,12 @@ model Invoice {
|
|||||||
paymentId String? @unique
|
paymentId String? @unique
|
||||||
amount Float
|
amount Float
|
||||||
currency String
|
currency String
|
||||||
status String // PENDING, PAID, FAILED, CANCELLED
|
status String // PENDING, PAID, FAILED, CANCELLED
|
||||||
billingPeriodStart DateTime
|
billingPeriodStart DateTime
|
||||||
billingPeriodEnd DateTime
|
billingPeriodEnd DateTime
|
||||||
dueDate DateTime
|
dueDate DateTime
|
||||||
paidAt DateTime?
|
paidAt DateTime?
|
||||||
items Json // Array of line items
|
items Json // Array of line items
|
||||||
attempts Int @default(0)
|
attempts Int @default(0)
|
||||||
failureReason String?
|
failureReason String?
|
||||||
metadata Json?
|
metadata Json?
|
||||||
@ -125,29 +125,29 @@ model Invoice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Subscription {
|
model Subscription {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String
|
userId String
|
||||||
planId String
|
planId String
|
||||||
partnerId String
|
partnerId String
|
||||||
status SubscriptionStatus
|
status SubscriptionStatus
|
||||||
currentPeriodStart DateTime
|
currentPeriodStart DateTime
|
||||||
currentPeriodEnd DateTime
|
currentPeriodEnd DateTime
|
||||||
nextBillingDate DateTime?
|
nextBillingDate DateTime?
|
||||||
trialEndsAt DateTime?
|
trialEndsAt DateTime?
|
||||||
cancelledAt DateTime?
|
cancelledAt DateTime?
|
||||||
suspendedAt DateTime?
|
suspendedAt DateTime?
|
||||||
failureCount Int @default(0)
|
failureCount Int @default(0)
|
||||||
renewalCount Int @default(0)
|
renewalCount Int @default(0)
|
||||||
lastPaymentId String?
|
lastPaymentId String?
|
||||||
metadata Json?
|
metadata Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
plan Plan @relation(fields: [planId], references: [id])
|
plan Plan @relation(fields: [planId], references: [id])
|
||||||
partner Partner @relation(fields: [partnerId], references: [id])
|
partner Partner @relation(fields: [partnerId], references: [id])
|
||||||
payments Payment[]
|
payments Payment[]
|
||||||
invoices Invoice[]
|
invoices Invoice[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Payment {
|
model Payment {
|
||||||
@ -166,7 +166,7 @@ model Payment {
|
|||||||
completedAt DateTime?
|
completedAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
partner Partner @relation(fields: [partnerId], references: [id])
|
partner Partner @relation(fields: [partnerId], references: [id])
|
||||||
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
|
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
|
||||||
@ -182,42 +182,42 @@ model Refund {
|
|||||||
status String
|
status String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
payment Payment @relation(fields: [paymentId], references: [id])
|
payment Payment @relation(fields: [paymentId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Webhook {
|
model Webhook {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
partnerId String? // Made optional for system webhooks
|
partnerId String? // Made optional for system webhooks
|
||||||
url String
|
url String
|
||||||
event String
|
event String
|
||||||
payload Json
|
payload Json
|
||||||
response Json?
|
response Json?
|
||||||
status String
|
status String
|
||||||
attempts Int @default(0)
|
attempts Int @default(0)
|
||||||
lastAttempt DateTime?
|
lastAttempt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
partner Partner? @relation(fields: [partnerId], references: [id])
|
partner Partner? @relation(fields: [partnerId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Partner {
|
model Partner {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
email String @unique
|
email String @unique
|
||||||
passwordHash String
|
passwordHash String
|
||||||
apiKey String @unique
|
apiKey String @unique
|
||||||
secretKey String
|
secretKey String
|
||||||
status String @default("PENDING")
|
status String @default("PENDING")
|
||||||
companyInfo Json?
|
companyInfo Json?
|
||||||
callbacks Json?
|
callbacks Json?
|
||||||
country String
|
country String
|
||||||
metadata Json?
|
metadata Json?
|
||||||
keysRotatedAt DateTime?
|
keysRotatedAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
users User[]
|
users User[]
|
||||||
subscriptions Subscription[]
|
subscriptions Subscription[]
|
||||||
payments Payment[]
|
payments Payment[]
|
||||||
@ -225,7 +225,7 @@ model Partner {
|
|||||||
plans Plan[]
|
plans Plan[]
|
||||||
invoices Invoice[]
|
invoices Invoice[]
|
||||||
notifications Notification[] // Added relation
|
notifications Notification[] // Added relation
|
||||||
webhooks Webhook[] // Added relation
|
webhooks Webhook[] // Added relation
|
||||||
}
|
}
|
||||||
|
|
||||||
model AuthSession {
|
model AuthSession {
|
||||||
@ -242,7 +242,7 @@ model AuthSession {
|
|||||||
expiresAt DateTime
|
expiresAt DateTime
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
partner Partner @relation(fields: [partnerId], references: [id])
|
partner Partner @relation(fields: [partnerId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,13 +250,13 @@ model Notification {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
partnerId String
|
partnerId String
|
||||||
userId String?
|
userId String?
|
||||||
type String // PAYMENT, SUBSCRIPTION, ALERT, MARKETING
|
type String // PAYMENT, SUBSCRIPTION, ALERT, MARKETING
|
||||||
channel String // SMS, EMAIL, WEBHOOK
|
channel String // SMS, EMAIL, WEBHOOK
|
||||||
recipient String
|
recipient String
|
||||||
subject String?
|
subject String?
|
||||||
content String
|
content String
|
||||||
templateId String?
|
templateId String?
|
||||||
status String // PENDING, SENT, FAILED
|
status String // PENDING, SENT, FAILED
|
||||||
batchId String?
|
batchId String?
|
||||||
scheduledFor DateTime?
|
scheduledFor DateTime?
|
||||||
sentAt DateTime?
|
sentAt DateTime?
|
||||||
@ -266,7 +266,7 @@ model Notification {
|
|||||||
metadata Json?
|
metadata Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
partner Partner @relation(fields: [partnerId], references: [id])
|
partner Partner @relation(fields: [partnerId], references: [id])
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation(fields: [userId], references: [id])
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/modules/operators/adapters/mtn.adapter.ts
Normal file
3
src/modules/operators/adapters/mtn.adapter.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export class MTNAdapter{
|
||||||
|
|
||||||
|
}
|
||||||
5
src/modules/operators/operators.controller.ts
Normal file
5
src/modules/operators/operators.controller.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//todo
|
||||||
|
|
||||||
|
export class OperatorsController{
|
||||||
|
|
||||||
|
}
|
||||||
7
src/modules/operators/operators.service.ts
Normal file
7
src/modules/operators/operators.service.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//todo tomaj
|
||||||
|
export class OperatorsService{
|
||||||
|
getAdapter(code: any, country: any) :any{
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
4
src/modules/operators/transformers/mtn.transformer.ts
Normal file
4
src/modules/operators/transformers/mtn.transformer.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export class MTNTransformer{
|
||||||
|
|
||||||
|
}
|
||||||
3
src/modules/operators/transformers/orange.transformer.ts
Normal file
3
src/modules/operators/transformers/orange.transformer.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export class OrangeTransformer{
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import { IsString, IsNumber, IsOptional, IsUrl, Min } from 'class-validator';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
export class ChargeDto {
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
userToken: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
amount: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
currency: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
reference?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
|
||||||
@IsOptional()
|
|
||||||
@IsUrl()
|
|
||||||
callbackUrl?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
|
||||||
@IsOptional()
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
|
||||||
161
src/modules/payments/dto/payment.dto.ts
Normal file
161
src/modules/payments/dto/payment.dto.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsUrl,
|
||||||
|
Min,
|
||||||
|
IsEnum,
|
||||||
|
IsDateString,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class ChargeDto {
|
||||||
|
@ApiProperty({ description: 'User token from authentication' })
|
||||||
|
@IsString()
|
||||||
|
userToken: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Amount to charge' })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Currency code (XOF, XAF, USD, etc.)' })
|
||||||
|
@IsString()
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Payment description' })
|
||||||
|
@IsString()
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Unique payment reference' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
reference?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Subscription ID if recurring' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
subscriptionId?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Callback URL for notifications' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsUrl()
|
||||||
|
callbackUrl?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Additional metadata' })
|
||||||
|
@IsOptional()
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'partnerId ' })
|
||||||
|
@IsOptional()
|
||||||
|
partnerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RefundDto {
|
||||||
|
@ApiProperty({ required: false, description: 'Amount to refund (partial refund)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
amount?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Reason for refund' })
|
||||||
|
@IsString()
|
||||||
|
reason: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'Additional metadata' })
|
||||||
|
@IsOptional()
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaymentQueryDto {
|
||||||
|
@ApiProperty({ required: false, enum: ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED'] })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED'])
|
||||||
|
status?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
userId?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
subscriptionId?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
startDate?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
endDate?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: 20 })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
limit?: number = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaymentResponseDto {
|
||||||
|
@ApiProperty()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
reference: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
operatorReference?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED'] })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
failureReason?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
completedAt?: Date;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaymentListResponseDto {
|
||||||
|
@ApiProperty({ type: [PaymentResponseDto] })
|
||||||
|
data: PaymentResponseDto[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,204 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Get,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
Request,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
BadRequestException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiResponse,
|
||||||
|
ApiQuery,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { PaymentsService } from './payments.service';
|
||||||
|
import {
|
||||||
|
ChargeDto,
|
||||||
|
RefundDto,
|
||||||
|
PaymentQueryDto,
|
||||||
|
PaymentResponseDto,
|
||||||
|
PaymentListResponseDto,
|
||||||
|
} from './dto/payment.dto';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { ApiKeyGuard } from '../../common/guards/api-key.guard';
|
||||||
|
|
||||||
|
@ApiTags('payments')
|
||||||
|
@Controller('payments')
|
||||||
|
export class PaymentsController {
|
||||||
|
constructor(private readonly paymentsService: PaymentsService) {}
|
||||||
|
|
||||||
|
@Post('charge')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({ summary: 'Create a new charge' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Payment created successfully',
|
||||||
|
type: PaymentResponseDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: 'Bad request' })
|
||||||
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||||
|
async createCharge(@Request() req, @Body() chargeDto: ChargeDto) {
|
||||||
|
return this.paymentsService.createCharge({
|
||||||
|
...chargeDto,
|
||||||
|
partnerId: req.user.partnerId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':paymentId/refund')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Refund a payment' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Refund processed successfully',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Payment not found' })
|
||||||
|
async refundPayment(
|
||||||
|
@Request() req,
|
||||||
|
@Param('paymentId') paymentId: string,
|
||||||
|
@Body() refundDto: RefundDto,
|
||||||
|
) {
|
||||||
|
return this.paymentsService.refundPayment(
|
||||||
|
paymentId,
|
||||||
|
req.user.partnerId,
|
||||||
|
refundDto,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':paymentId')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Get payment details' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Payment details retrieved',
|
||||||
|
type: PaymentResponseDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Payment not found' })
|
||||||
|
async getPayment(@Request() req, @Param('paymentId') paymentId: string) {
|
||||||
|
return this.paymentsService.getPayment(paymentId, req.user.partnerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'List payments' })
|
||||||
|
@ApiQuery({ name: 'status', required: false, enum: ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED'] })
|
||||||
|
@ApiQuery({ name: 'userId', required: false })
|
||||||
|
@ApiQuery({ name: 'subscriptionId', required: false })
|
||||||
|
@ApiQuery({ name: 'startDate', required: false, type: Date })
|
||||||
|
@ApiQuery({ name: 'endDate', required: false, type: Date })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number, default: 1 })
|
||||||
|
@ApiQuery({ name: 'limit', required: false, type: Number, default: 20 })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'List of payments',
|
||||||
|
type: PaymentListResponseDto,
|
||||||
|
})
|
||||||
|
async listPayments(@Request() req, @Query() query: PaymentQueryDto) {
|
||||||
|
return this.paymentsService.listPayments({
|
||||||
|
partnerId: req.user.partnerId,
|
||||||
|
...query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('reference/:reference')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Get payment by reference' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Payment details retrieved',
|
||||||
|
type: PaymentResponseDto,
|
||||||
|
})
|
||||||
|
async getPaymentByReference(
|
||||||
|
@Request() req,
|
||||||
|
@Param('reference') reference: string,
|
||||||
|
) {
|
||||||
|
return this.paymentsService.getPaymentByReference(
|
||||||
|
reference,
|
||||||
|
req.user.partnerId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':paymentId/retry')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Retry a failed payment' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Payment retry initiated',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: 'Payment cannot be retried' })
|
||||||
|
async retryPayment(
|
||||||
|
@Request() req,
|
||||||
|
@Param('paymentId') paymentId: string,
|
||||||
|
) {
|
||||||
|
return this.paymentsService.retryPayment(paymentId, req.user.partnerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('statistics/summary')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Get payment statistics' })
|
||||||
|
@ApiQuery({ name: 'period', required: false, enum: ['daily', 'weekly', 'monthly', 'yearly'] })
|
||||||
|
@ApiQuery({ name: 'startDate', required: false, type: Date })
|
||||||
|
@ApiQuery({ name: 'endDate', required: false, type: Date })
|
||||||
|
async getStatistics(
|
||||||
|
@Request() req,
|
||||||
|
@Query('period') period?: string,
|
||||||
|
@Query('startDate') startDate?: string,
|
||||||
|
@Query('endDate') endDate?: string,
|
||||||
|
) {
|
||||||
|
return this.paymentsService.getStatistics({
|
||||||
|
partnerId: req.user.partnerId,
|
||||||
|
period: period || 'monthly',
|
||||||
|
startDate: startDate ? new Date(startDate) : undefined,
|
||||||
|
endDate: endDate ? new Date(endDate) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('validate')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Validate payment before processing' })
|
||||||
|
async validatePayment(@Request() req, @Body() chargeDto: ChargeDto) {
|
||||||
|
return this.paymentsService.validatePayment({
|
||||||
|
...chargeDto,
|
||||||
|
partnerId: req.user.partnerId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhook endpoints
|
||||||
|
@Post('webhook/callback')
|
||||||
|
@UseGuards(ApiKeyGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Webhook callback for payment updates' })
|
||||||
|
async handleWebhook(@Request() req, @Body() payload: any) {
|
||||||
|
const signature = req.headers['x-webhook-signature'];
|
||||||
|
const event = req.headers['x-webhook-event'];
|
||||||
|
|
||||||
|
if (!signature || !event) {
|
||||||
|
throw new BadRequestException('Missing webhook headers');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.paymentsService.handleWebhook({
|
||||||
|
partnerId: req.partner.id,
|
||||||
|
event,
|
||||||
|
payload,
|
||||||
|
signature,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,11 +2,30 @@ import { Injectable, BadRequestException } from '@nestjs/common';
|
|||||||
import { OperatorsService } from '../operators/operators.service';
|
import { OperatorsService } from '../operators/operators.service';
|
||||||
import { PrismaService } from '../../shared/services/prisma.service';
|
import { PrismaService } from '../../shared/services/prisma.service';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { ChargeDto } from './dto/charge.dto';
|
import { ChargeDto } from './dto/payment.dto';
|
||||||
import { PaymentStatus } from '@prisma/client';
|
import { RefundDto } from './dto/payment.dto';
|
||||||
|
import { PaymentStatus } from 'generated/prisma';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaymentsService {
|
export class PaymentsService {
|
||||||
|
handleWebhook(arg0: { partnerId: any; event: any; payload: any; signature: any; }) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
getPaymentByReference(reference: string, partnerId: any) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
getPayment(paymentId: string, partnerId: any) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
refundPayment(paymentId: string, partnerId: any, refundDto: RefundDto) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
retryPayment(paymentId: any, attempt: any) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
processPayment(paymentId: any) :any{
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
constructor(
|
constructor(
|
||||||
private readonly operatorsService: OperatorsService,
|
private readonly operatorsService: OperatorsService,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
@ -102,4 +121,166 @@ export class PaymentsService {
|
|||||||
// Implémenter la notification webhook
|
// Implémenter la notification webhook
|
||||||
// Utiliser Bull Queue pour gérer les retries
|
// Utiliser Bull Queue pour gérer les retries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ajouter ces méthodes dans PaymentsService
|
||||||
|
|
||||||
|
async listPayments(filters: any) {
|
||||||
|
const where: any = {
|
||||||
|
partnerId: filters.partnerId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filters.status) {
|
||||||
|
where.status = filters.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.userId) {
|
||||||
|
where.userId = filters.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.subscriptionId) {
|
||||||
|
where.subscriptionId = filters.subscriptionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.startDate || filters.endDate) {
|
||||||
|
where.createdAt = {};
|
||||||
|
if (filters.startDate) {
|
||||||
|
where.createdAt.gte = new Date(filters.startDate);
|
||||||
|
}
|
||||||
|
if (filters.endDate) {
|
||||||
|
where.createdAt.lte = new Date(filters.endDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = filters.page || 1;
|
||||||
|
const limit = filters.limit || 20;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const [payments, total] = await Promise.all([
|
||||||
|
this.prisma.payment.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
msisdn: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subscription: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
planId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.prisma.payment.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: payments,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatistics(params: {
|
||||||
|
partnerId: string;
|
||||||
|
period: string;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
}) {
|
||||||
|
const { partnerId, period, startDate, endDate } = params;
|
||||||
|
|
||||||
|
const where: any = { partnerId };
|
||||||
|
|
||||||
|
if (startDate || endDate) {
|
||||||
|
where.createdAt = {};
|
||||||
|
if (startDate) where.createdAt.gte = startDate;
|
||||||
|
if (endDate) where.createdAt.lte = endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
totalPayments,
|
||||||
|
successfulPayments,
|
||||||
|
failedPayments,
|
||||||
|
totalRevenue,
|
||||||
|
avgPaymentAmount,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.prisma.payment.count({ where }),
|
||||||
|
this.prisma.payment.count({ where: { ...where, status: 'SUCCESS' } }),
|
||||||
|
this.prisma.payment.count({ where: { ...where, status: 'FAILED' } }),
|
||||||
|
this.prisma.payment.aggregate({
|
||||||
|
where: { ...where, status: 'SUCCESS' },
|
||||||
|
_sum: { amount: true },
|
||||||
|
}),
|
||||||
|
this.prisma.payment.aggregate({
|
||||||
|
where: { ...where, status: 'SUCCESS' },
|
||||||
|
_avg: { amount: true },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const successRate = totalPayments > 0
|
||||||
|
? (successfulPayments / totalPayments) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalPayments,
|
||||||
|
successfulPayments,
|
||||||
|
failedPayments,
|
||||||
|
successRate: Math.round(successRate * 100) / 100,
|
||||||
|
totalRevenue: totalRevenue._sum.amount || 0,
|
||||||
|
avgPaymentAmount: avgPaymentAmount._avg.amount || 0,
|
||||||
|
period,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async validatePayment(params: any) {
|
||||||
|
// Valider le user token
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { userToken: params.userToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Invalid user token',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les limites
|
||||||
|
const todayPayments = await this.prisma.payment.count({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
status: 'SUCCESS',
|
||||||
|
createdAt: {
|
||||||
|
gte: new Date(new Date().setHours(0, 0, 0, 0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (todayPayments >= 10) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Daily payment limit reached',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
msisdn: user.msisdn,
|
||||||
|
country: user.country,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Process, Processor } from '@nestjs/bull';
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
import { Job } from 'bull';
|
import bull from 'bull';
|
||||||
import { PaymentsService } from '../payments.service';
|
import { PaymentsService } from '../payments.service';
|
||||||
import { WebhookService } from '../services/webhook.service';
|
import { WebhookService } from '../services/webhook.service';
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ export class PaymentProcessor {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process('process-payment')
|
@Process('process-payment')
|
||||||
async handlePayment(job: Job) {
|
async handlePayment(job: bull.Job) {
|
||||||
const { paymentId } = job.data;
|
const { paymentId } = job.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -36,7 +36,7 @@ export class PaymentProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Process('retry-payment')
|
@Process('retry-payment')
|
||||||
async handleRetry(job: Job) {
|
async handleRetry(job: bull.Job) {
|
||||||
const { paymentId, attempt } = job.data;
|
const { paymentId, attempt } = job.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
8
src/modules/payments/services/webhook.service.ts
Normal file
8
src/modules/payments/services/webhook.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//todo
|
||||||
|
|
||||||
|
export class WebhookService{
|
||||||
|
send(arg0: { url: any; event: string; payload: any; }) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,15 +1,15 @@
|
|||||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../../../shared/services/prisma.service';
|
import { PrismaService } from '../../../shared/services/prisma.service';
|
||||||
import { PaymentsService } from '../../payments/payments.service';
|
import { PaymentsService } from '../../payments/payments.service';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Queue } from 'bull';
|
import bull from 'bull';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingService {
|
export class BillingService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly paymentsService: PaymentsService,
|
private readonly paymentsService: PaymentsService,
|
||||||
@InjectQueue('billing') private billingQueue: Queue,
|
@InjectQueue('billing') private billingQueue: bull.Queue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async processBilling(subscriptionId: string) {
|
async processBilling(subscriptionId: string) {
|
||||||
|
|||||||
@ -1,22 +1,24 @@
|
|||||||
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Queue } from 'bull';
|
import bull from 'bull';
|
||||||
import { PrismaService } from '../../shared/services/prisma.service';
|
import { PrismaService } from '../../shared/services/prisma.service';
|
||||||
import { PaymentsService } from '../payments/payments.service';
|
import { PaymentsService } from '../payments/payments.service';
|
||||||
import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto';
|
import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto';
|
||||||
import { SubscriptionStatus } from '@prisma/client';
|
//import { SubscriptionStatus } from '@prisma/client';
|
||||||
|
//import { SubscriptionStatus, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubscriptionsService {
|
export class SubscriptionsService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly paymentsService: PaymentsService,
|
private readonly paymentsService: PaymentsService,
|
||||||
@InjectQueue('subscriptions') private subscriptionQueue: Queue,
|
@InjectQueue('subscriptions') private subscriptionQueue: bull.Queue,
|
||||||
@InjectQueue('billing') private billingQueue: Queue,
|
@InjectQueue('billing') private billingQueue: bull.Queue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(partnerId: string, dto: CreateSubscriptionDto) {
|
async create(partnerId: string, dto: CreateSubscriptionDto) {
|
||||||
// Vérifier l'utilisateur
|
// Vérifier l'utilisateur
|
||||||
|
|
||||||
const user = await this.prisma.user.findFirst({
|
const user = await this.prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userToken: dto.userToken,
|
userToken: dto.userToken,
|
||||||
|
|||||||
@ -2,11 +2,9 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
extends PrismaClient
|
|
||||||
implements OnModuleInit, OnModuleDestroy
|
|
||||||
{
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
super({
|
super({
|
||||||
log: ['query', 'info', 'warn', 'error'],
|
log: ['query', 'info', 'warn', 'error'],
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user