first commit
This commit is contained in:
parent
ad91d5c150
commit
bde4f90235
@ -2,6 +2,7 @@
|
||||
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(
|
||||
@ -27,6 +28,8 @@ export default tseslint.config(
|
||||
{
|
||||
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', {
|
||||
|
||||
67
package-lock.json
generated
67
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/bull": "^11.0.4",
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
@ -23,6 +24,7 @@
|
||||
"@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",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
@ -2192,6 +2194,17 @@
|
||||
"@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": {
|
||||
"version": "11.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-11.0.4.tgz",
|
||||
@ -4461,9 +4474,20 @@
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
||||
@ -5031,6 +5055,12 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
|
||||
@ -5211,7 +5241,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
@ -5524,7 +5553,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@ -5783,7 +5811,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@ -6428,6 +6455,27 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@ -6477,7 +6525,6 @@
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
@ -6494,7 +6541,6 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@ -6504,7 +6550,6 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
@ -6879,7 +6924,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@ -9490,6 +9534,13 @@
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
"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",
|
||||
@ -34,6 +35,7 @@
|
||||
"@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",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
|
||||
@ -3,3 +3,6 @@ npx prisma migrate dev --name init
|
||||
|
||||
# Générer le client Prisma
|
||||
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;
|
||||
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 { PrismaService } from '../../shared/services/prisma.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { ChargeDto } from './dto/charge.dto';
|
||||
import { PaymentStatus } from '@prisma/client';
|
||||
import { ChargeDto } from './dto/payment.dto';
|
||||
import { RefundDto } from './dto/payment.dto';
|
||||
import { PaymentStatus } from 'generated/prisma';
|
||||
|
||||
@Injectable()
|
||||
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(
|
||||
private readonly operatorsService: OperatorsService,
|
||||
private readonly prisma: PrismaService,
|
||||
@ -102,4 +121,166 @@ export class PaymentsService {
|
||||
// Implémenter la notification webhook
|
||||
// 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 { Job } from 'bull';
|
||||
import bull from 'bull';
|
||||
import { PaymentsService } from '../payments.service';
|
||||
import { WebhookService } from '../services/webhook.service';
|
||||
|
||||
@ -11,7 +11,7 @@ export class PaymentProcessor {
|
||||
) {}
|
||||
|
||||
@Process('process-payment')
|
||||
async handlePayment(job: Job) {
|
||||
async handlePayment(job: bull.Job) {
|
||||
const { paymentId } = job.data;
|
||||
|
||||
try {
|
||||
@ -36,7 +36,7 @@ export class PaymentProcessor {
|
||||
}
|
||||
|
||||
@Process('retry-payment')
|
||||
async handleRetry(job: Job) {
|
||||
async handleRetry(job: bull.Job) {
|
||||
const { paymentId, attempt } = job.data;
|
||||
|
||||
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 { PaymentsService } from '../../payments/payments.service';
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Queue } from 'bull';
|
||||
import bull from 'bull';
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly paymentsService: PaymentsService,
|
||||
@InjectQueue('billing') private billingQueue: Queue,
|
||||
@InjectQueue('billing') private billingQueue: bull.Queue,
|
||||
) {}
|
||||
|
||||
async processBilling(subscriptionId: string) {
|
||||
|
||||
@ -1,22 +1,24 @@
|
||||
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Queue } from 'bull';
|
||||
import bull from 'bull';
|
||||
import { PrismaService } from '../../shared/services/prisma.service';
|
||||
import { PaymentsService } from '../payments/payments.service';
|
||||
import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto';
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
//import { SubscriptionStatus } from '@prisma/client';
|
||||
//import { SubscriptionStatus, Prisma } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly paymentsService: PaymentsService,
|
||||
@InjectQueue('subscriptions') private subscriptionQueue: Queue,
|
||||
@InjectQueue('billing') private billingQueue: Queue,
|
||||
@InjectQueue('subscriptions') private subscriptionQueue: bull.Queue,
|
||||
@InjectQueue('billing') private billingQueue: bull.Queue,
|
||||
) {}
|
||||
|
||||
async create(partnerId: string, dto: CreateSubscriptionDto) {
|
||||
// Vérifier l'utilisateur
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
userToken: dto.userToken,
|
||||
|
||||
@ -2,11 +2,9 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService
|
||||
extends PrismaClient
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
constructor() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
super({
|
||||
log: ['query', 'info', 'warn', 'error'],
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user