fix it
This commit is contained in:
parent
1bede21bbb
commit
29e82bc746
@ -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(
|
||||||
@ -17,9 +18,9 @@ export default tseslint.config(
|
|||||||
...globals.node,
|
...globals.node,
|
||||||
...globals.jest,
|
...globals.jest,
|
||||||
},
|
},
|
||||||
sourceType: 'commonjs',
|
sourceType: 'module', // Changé de 'commonjs' à 'module'
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
projectService: true,
|
project: './tsconfig.json', // Ajout du chemin vers tsconfig
|
||||||
tsconfigRootDir: import.meta.dirname,
|
tsconfigRootDir: import.meta.dirname,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -27,8 +28,16 @@ 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', {
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'all',
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
704
package-lock.json
generated
704
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@
|
|||||||
"name": "reporting-api",
|
"name": "reporting-api",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
"type": "commonjs",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
@ -26,12 +27,12 @@
|
|||||||
"@nestjs/mongoose": "^11.0.3",
|
"@nestjs/mongoose": "^11.0.3",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/schedule": "^6.0.1",
|
"@nestjs/schedule": "^6.0.1",
|
||||||
"@prisma/client": "^7.0.1",
|
"@nestjs/swagger": "^11.2.3",
|
||||||
|
"@prisma/client": "^6.17.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"mongoose": "^9.0.0",
|
"prisma": "^6.17.1",
|
||||||
"prisma": "^7.0.1",
|
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,15 +5,17 @@
|
|||||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client"
|
provider = "prisma-client-js"
|
||||||
output = "../generated/prisma"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL") // ← AJOUTEZ CETTE LIGNE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model Transaction {
|
model Transaction {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
date DateTime @default(now())
|
date DateTime @default(now())
|
||||||
|
|||||||
@ -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"]
|
||||||
@ -1,10 +1,51 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AppService } from './app.service';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { MongodbModule } from './mongodb/mongodb.module';
|
||||||
|
import { SyncModule } from './sync/sync.module';
|
||||||
|
import { ReportingModule } from './reporting/reporting.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
controllers: [AppController],
|
// Configuration globale
|
||||||
providers: [AppService],
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
envFilePath: '.env',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Scheduler pour la synchronisation automatique
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
|
||||||
|
// Connexion MongoDB
|
||||||
|
MongooseModule.forRootAsync({
|
||||||
|
useFactory: () => ({
|
||||||
|
uri: process.env.MONGODB_URI,
|
||||||
|
/**
|
||||||
|
|
||||||
|
|
||||||
|
connectionFactory: (connection) => {
|
||||||
|
connection.on('connected', () => {
|
||||||
|
console.log('✅ MongoDB connected');
|
||||||
|
});
|
||||||
|
connection.on('error', (error) => {
|
||||||
|
console.error('❌ MongoDB connection error:', error);
|
||||||
|
});
|
||||||
|
connection.on('disconnected', () => {
|
||||||
|
console.log('❌ MongoDB disconnected');
|
||||||
|
});
|
||||||
|
return connection;
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Modules de l'application
|
||||||
|
PrismaModule,
|
||||||
|
MongodbModule,
|
||||||
|
SyncModule,
|
||||||
|
ReportingModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
46
src/main.ts
46
src/main.ts
@ -1,8 +1,52 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
|
||||||
|
// 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 || 3004;
|
||||||
|
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();
|
bootstrap();
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
import {
|
||||||
|
TransactionDoc,
|
||||||
|
TransactionSchema,
|
||||||
|
} from './schemas/transaction.schema';
|
||||||
|
import {
|
||||||
|
SubscriptionDoc,
|
||||||
|
SubscriptionSchema,
|
||||||
|
} from './schemas/subscription.schema';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
{ name: TransactionDoc.name, schema: TransactionSchema },
|
||||||
|
{ name: SubscriptionDoc.name, schema: SubscriptionSchema },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
exports: [MongooseModule],
|
||||||
|
})
|
||||||
|
export class MongodbModule {}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||||
|
import { Document } from 'mongoose';
|
||||||
|
|
||||||
|
@Schema({ collection: 'subscriptions', timestamps: true })
|
||||||
|
export class SubscriptionDoc extends Document {
|
||||||
|
@Prop({ required: true, unique: true })
|
||||||
|
subscriptionId: number;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
startDate: Date;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
endDate: Date;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
periodicity: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
merchantPartnerId: number;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
year: number;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
month: number;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
week: number;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
day: string; // Format: YYYY-MM-DD
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubscriptionSchema = SchemaFactory.createForClass(SubscriptionDoc);
|
||||||
|
|
||||||
|
// Index pour optimiser les requêtes
|
||||||
|
SubscriptionSchema.index({ merchantPartnerId: 1, startDate: -1 });
|
||||||
|
SubscriptionSchema.index({ startDate: -1 });
|
||||||
|
SubscriptionSchema.index({ day: 1, merchantPartnerId: 1 });
|
||||||
|
SubscriptionSchema.index({ year: 1, month: 1 });
|
||||||
|
SubscriptionSchema.index({ year: 1, week: 1 });
|
||||||
|
SubscriptionSchema.index({ status: 1 });
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||||
|
import { Document } from 'mongoose';
|
||||||
|
|
||||||
|
@Schema({ collection: 'transactions', timestamps: true })
|
||||||
|
export class TransactionDoc extends Document {
|
||||||
|
@Prop({ required: true, unique: true })
|
||||||
|
transactionId: number;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
date: Date;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
tax: number;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
merchantPartnerId: number;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
year: number;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
month: number;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
week: number;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
day: string; // Format: YYYY-MM-DD
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransactionSchema = SchemaFactory.createForClass(TransactionDoc);
|
||||||
|
|
||||||
|
// Index pour optimiser les requêtes
|
||||||
|
TransactionSchema.index({ merchantPartnerId: 1, date: -1 });
|
||||||
|
TransactionSchema.index({ date: -1 });
|
||||||
|
TransactionSchema.index({ day: 1, merchantPartnerId: 1 });
|
||||||
|
TransactionSchema.index({ year: 1, month: 1 });
|
||||||
|
TransactionSchema.index({ year: 1, week: 1 });
|
||||||
|
TransactionSchema.index({ status: 1 });
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Module, Global } from '@nestjs/common';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
//import { PrismaClient } from 'generated/prisma/client';
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService
|
||||||
|
extends PrismaClient
|
||||||
|
implements OnModuleInit, OnModuleDestroy
|
||||||
|
{
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
console.log('✅ PostgreSQL connected via Prisma');
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.$disconnect();
|
||||||
|
console.log('❌ PostgreSQL disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { IsEnum, IsOptional, IsInt, IsDateString } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export enum ReportPeriod {
|
||||||
|
DAILY = 'daily',
|
||||||
|
WEEKLY = 'weekly',
|
||||||
|
MONTHLY = 'monthly',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ReportType {
|
||||||
|
TRANSACTION = 'transaction',
|
||||||
|
SUBSCRIPTION = 'subscription',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReportQueryDto {
|
||||||
|
@IsEnum(ReportPeriod)
|
||||||
|
period: ReportPeriod;
|
||||||
|
|
||||||
|
@IsEnum(ReportType)
|
||||||
|
type: ReportType;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
merchantPartnerId?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
startDate?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
endDate?: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
export class ReportItemDto {
|
||||||
|
period: string; // "2024-01-15", "2024-W03", "2024-01"
|
||||||
|
totalAmount: number;
|
||||||
|
totalTax?: number;
|
||||||
|
count: number;
|
||||||
|
successCount?: number;
|
||||||
|
failedCount?: number;
|
||||||
|
pendingCount?: number;
|
||||||
|
activeCount?: number;
|
||||||
|
cancelledCount?: number;
|
||||||
|
merchantPartnerId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReportResponseDto {
|
||||||
|
type: string; // 'transaction' | 'subscription'
|
||||||
|
period: string; // 'daily' | 'weekly' | 'monthly'
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
merchantPartnerId?: number;
|
||||||
|
totalAmount: number;
|
||||||
|
totalCount: number;
|
||||||
|
items: ReportItemDto[];
|
||||||
|
generatedAt: Date;
|
||||||
|
summary?: {
|
||||||
|
avgAmount?: number;
|
||||||
|
minAmount?: number;
|
||||||
|
maxAmount?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
Post,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ReportingService } from './reporting.service';
|
||||||
|
import { ReportPeriod, ReportType } from './dto/report-query.dto';
|
||||||
|
import { ReportResponseDto } from './dto/report-response.dto';
|
||||||
|
import { SyncService } from '../sync/sync.service';
|
||||||
|
|
||||||
|
@Controller('reporting')
|
||||||
|
export class ReportingController {
|
||||||
|
private readonly logger = new Logger(ReportingController.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private reportingService: ReportingService,
|
||||||
|
private syncService: SyncService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('transactions/daily')
|
||||||
|
async getTransactionsDaily(
|
||||||
|
@Query('merchantPartnerId') merchantPartnerId?: string,
|
||||||
|
@Query('startDate') startDate?: string,
|
||||||
|
@Query('endDate') endDate?: string,
|
||||||
|
): Promise<ReportResponseDto> {
|
||||||
|
return this.reportingService.generateReport({
|
||||||
|
period: ReportPeriod.DAILY,
|
||||||
|
type: ReportType.TRANSACTION,
|
||||||
|
merchantPartnerId: merchantPartnerId
|
||||||
|
? parseInt(merchantPartnerId)
|
||||||
|
: undefined,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('transactions/weekly')
|
||||||
|
async getTransactionsWeekly(
|
||||||
|
@Query('merchantPartnerId') merchantPartnerId?: string,
|
||||||
|
@Query('startDate') startDate?: string,
|
||||||
|
@Query('endDate') endDate?: string,
|
||||||
|
): Promise<ReportResponseDto> {
|
||||||
|
return this.reportingService.generateReport({
|
||||||
|
period: ReportPeriod.WEEKLY,
|
||||||
|
type: ReportType.TRANSACTION,
|
||||||
|
merchantPartnerId: merchantPartnerId
|
||||||
|
? parseInt(merchantPartnerId)
|
||||||
|
: undefined,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('transactions/monthly')
|
||||||
|
async getTransactionsMonthly(
|
||||||
|
@Query('merchantPartnerId') merchantPartnerId?: string,
|
||||||
|
@Query('startDate') startDate?: string,
|
||||||
|
@Query('endDate') endDate?: string,
|
||||||
|
): Promise<ReportResponseDto> {
|
||||||
|
return this.reportingService.generateReport({
|
||||||
|
period: ReportPeriod.MONTHLY,
|
||||||
|
type: ReportType.TRANSACTION,
|
||||||
|
merchantPartnerId: merchantPartnerId
|
||||||
|
? parseInt(merchantPartnerId)
|
||||||
|
: undefined,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('subscriptions/daily')
|
||||||
|
async getSubscriptionsDaily(
|
||||||
|
@Query('merchantPartnerId') merchantPartnerId?: string,
|
||||||
|
@Query('startDate') startDate?: string,
|
||||||
|
@Query('endDate') endDate?: string,
|
||||||
|
): Promise<ReportResponseDto> {
|
||||||
|
return this.reportingService.generateReport({
|
||||||
|
period: ReportPeriod.DAILY,
|
||||||
|
type: ReportType.SUBSCRIPTION,
|
||||||
|
merchantPartnerId: merchantPartnerId
|
||||||
|
? parseInt(merchantPartnerId)
|
||||||
|
: undefined,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('subscriptions/weekly')
|
||||||
|
async getSubscriptionsWeekly(
|
||||||
|
@Query('merchantPartnerId') merchantPartnerId?: string,
|
||||||
|
@Query('startDate') startDate?: string,
|
||||||
|
@Query('endDate') endDate?: string,
|
||||||
|
): Promise<ReportResponseDto> {
|
||||||
|
return this.reportingService.generateReport({
|
||||||
|
period: ReportPeriod.WEEKLY,
|
||||||
|
type: ReportType.SUBSCRIPTION,
|
||||||
|
merchantPartnerId: merchantPartnerId
|
||||||
|
? parseInt(merchantPartnerId)
|
||||||
|
: undefined,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('subscriptions/monthly')
|
||||||
|
async getSubscriptionsMonthly(
|
||||||
|
@Query('merchantPartnerId') merchantPartnerId?: string,
|
||||||
|
@Query('startDate') startDate?: string,
|
||||||
|
@Query('endDate') endDate?: string,
|
||||||
|
): Promise<ReportResponseDto> {
|
||||||
|
return this.reportingService.generateReport({
|
||||||
|
period: ReportPeriod.MONTHLY,
|
||||||
|
type: ReportType.SUBSCRIPTION,
|
||||||
|
merchantPartnerId: merchantPartnerId
|
||||||
|
? parseInt(merchantPartnerId)
|
||||||
|
: undefined,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('sync/full')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async triggerFullSync(): Promise<{ message: string; timestamp: Date }> {
|
||||||
|
this.logger.log('🔄 Manual full sync triggered');
|
||||||
|
await this.syncService.fullSync();
|
||||||
|
return {
|
||||||
|
message: 'Full sync completed successfully',
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('sync/incremental')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async triggerIncrementalSync(): Promise<{
|
||||||
|
message: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}> {
|
||||||
|
this.logger.log('🔄 Manual incremental sync triggered');
|
||||||
|
await this.syncService.incrementalSync();
|
||||||
|
return {
|
||||||
|
message: 'Incremental sync completed successfully',
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/reporting/reporting.module.ts
Normal file
12
src/reporting/reporting.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MongodbModule } from '../mongodb/mongodb.module';
|
||||||
|
import { SyncModule } from '../sync/sync.module';
|
||||||
|
import { ReportingController } from './reporting.controller';
|
||||||
|
import { ReportingService } from './reporting.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [MongodbModule, SyncModule],
|
||||||
|
controllers: [ReportingController],
|
||||||
|
providers: [ReportingService],
|
||||||
|
})
|
||||||
|
export class ReportingModule {}
|
||||||
@ -0,0 +1,225 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
import { TransactionDoc } from '../mongodb/schemas/transaction.schema';
|
||||||
|
import { SubscriptionDoc } from '../mongodb/schemas/subscription.schema';
|
||||||
|
import {
|
||||||
|
ReportQueryDto,
|
||||||
|
ReportPeriod,
|
||||||
|
ReportType,
|
||||||
|
} from './dto/report-query.dto';
|
||||||
|
import { ReportResponseDto, ReportItemDto } from './dto/report-response.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ReportingService {
|
||||||
|
private readonly logger = new Logger(ReportingService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectModel(TransactionDoc.name)
|
||||||
|
private transactionModel: Model<TransactionDoc>,
|
||||||
|
@InjectModel(SubscriptionDoc.name)
|
||||||
|
private subscriptionModel: Model<SubscriptionDoc>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async generateReport(query: ReportQueryDto): Promise<ReportResponseDto> {
|
||||||
|
this.logger.log(`📊 Generating ${query.type} report - ${query.period}`);
|
||||||
|
|
||||||
|
const { period, type, merchantPartnerId, startDate, endDate } = query;
|
||||||
|
const dateFilter = this.buildDateFilter(startDate, endDate, type);
|
||||||
|
const merchantFilter = merchantPartnerId ? { merchantPartnerId } : {};
|
||||||
|
const filter = { ...dateFilter, ...merchantFilter };
|
||||||
|
|
||||||
|
if (type === ReportType.TRANSACTION) {
|
||||||
|
return this.generateTransactionReport(period, filter, query);
|
||||||
|
} else {
|
||||||
|
return this.generateSubscriptionReport(period, filter, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateTransactionReport(
|
||||||
|
period: ReportPeriod,
|
||||||
|
filter: any,
|
||||||
|
query: ReportQueryDto,
|
||||||
|
): Promise<ReportResponseDto> {
|
||||||
|
const groupBy = this.getGroupByField(period);
|
||||||
|
|
||||||
|
const pipeline: any[] = [
|
||||||
|
{ $match: filter },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: groupBy,
|
||||||
|
totalAmount: { $sum: '$amount' },
|
||||||
|
totalTax: { $sum: '$tax' },
|
||||||
|
count: { $sum: 1 },
|
||||||
|
successCount: {
|
||||||
|
$sum: { $cond: [{ $eq: ['$status', 'SUCCESS'] }, 1, 0] },
|
||||||
|
},
|
||||||
|
failedCount: {
|
||||||
|
$sum: { $cond: [{ $eq: ['$status', 'FAILED'] }, 1, 0] },
|
||||||
|
},
|
||||||
|
pendingCount: {
|
||||||
|
$sum: { $cond: [{ $eq: ['$status', 'PENDING'] }, 1, 0] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { _id: 1 } },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (filter.merchantPartnerId) {
|
||||||
|
pipeline[1].$group['merchantPartnerId'] = {
|
||||||
|
$first: '$merchantPartnerId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await this.transactionModel.aggregate(pipeline);
|
||||||
|
|
||||||
|
const items: ReportItemDto[] = results.map((item) => ({
|
||||||
|
period: this.formatPeriod(item._id, period),
|
||||||
|
totalAmount: Math.round(item.totalAmount * 100) / 100,
|
||||||
|
totalTax: Math.round(item.totalTax * 100) / 100,
|
||||||
|
count: item.count,
|
||||||
|
successCount: item.successCount,
|
||||||
|
failedCount: item.failedCount,
|
||||||
|
pendingCount: item.pendingCount,
|
||||||
|
merchantPartnerId: item.merchantPartnerId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totalAmount = items.reduce((sum, item) => sum + item.totalAmount, 0);
|
||||||
|
const totalCount = items.reduce((sum, item) => sum + item.count, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'transaction',
|
||||||
|
period: period,
|
||||||
|
startDate: query.startDate || '',
|
||||||
|
endDate: query.endDate || '',
|
||||||
|
merchantPartnerId: query.merchantPartnerId,
|
||||||
|
totalAmount: Math.round(totalAmount * 100) / 100,
|
||||||
|
totalCount,
|
||||||
|
items,
|
||||||
|
summary: {
|
||||||
|
avgAmount:
|
||||||
|
totalCount > 0
|
||||||
|
? Math.round((totalAmount / totalCount) * 100) / 100
|
||||||
|
: 0,
|
||||||
|
minAmount:
|
||||||
|
items.length > 0 ? Math.min(...items.map((i) => i.totalAmount)) : 0,
|
||||||
|
maxAmount:
|
||||||
|
items.length > 0 ? Math.max(...items.map((i) => i.totalAmount)) : 0,
|
||||||
|
},
|
||||||
|
generatedAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateSubscriptionReport(
|
||||||
|
period: ReportPeriod,
|
||||||
|
filter: any,
|
||||||
|
query: ReportQueryDto,
|
||||||
|
): Promise<ReportResponseDto> {
|
||||||
|
const groupBy = this.getGroupByField(period);
|
||||||
|
|
||||||
|
const pipeline: any[] = [
|
||||||
|
{ $match: filter },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: groupBy,
|
||||||
|
totalAmount: { $sum: '$amount' },
|
||||||
|
count: { $sum: 1 },
|
||||||
|
activeCount: {
|
||||||
|
$sum: { $cond: [{ $eq: ['$status', 'ACTIVE'] }, 1, 0] },
|
||||||
|
},
|
||||||
|
cancelledCount: {
|
||||||
|
$sum: { $cond: [{ $eq: ['$status', 'CANCELLED'] }, 1, 0] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { _id: 1 } },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (filter.merchantPartnerId) {
|
||||||
|
pipeline[1].$group['merchantPartnerId'] = {
|
||||||
|
$first: '$merchantPartnerId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await this.subscriptionModel.aggregate(pipeline);
|
||||||
|
|
||||||
|
const items: ReportItemDto[] = results.map((item) => ({
|
||||||
|
period: this.formatPeriod(item._id, period),
|
||||||
|
totalAmount: Math.round(item.totalAmount * 100) / 100,
|
||||||
|
count: item.count,
|
||||||
|
activeCount: item.activeCount,
|
||||||
|
cancelledCount: item.cancelledCount,
|
||||||
|
merchantPartnerId: item.merchantPartnerId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totalAmount = items.reduce((sum, item) => sum + item.totalAmount, 0);
|
||||||
|
const totalCount = items.reduce((sum, item) => sum + item.count, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'subscription',
|
||||||
|
period: period,
|
||||||
|
startDate: query.startDate || '',
|
||||||
|
endDate: query.endDate || '',
|
||||||
|
merchantPartnerId: query.merchantPartnerId,
|
||||||
|
totalAmount: Math.round(totalAmount * 100) / 100,
|
||||||
|
totalCount,
|
||||||
|
items,
|
||||||
|
summary: {
|
||||||
|
avgAmount:
|
||||||
|
totalCount > 0
|
||||||
|
? Math.round((totalAmount / totalCount) * 100) / 100
|
||||||
|
: 0,
|
||||||
|
minAmount:
|
||||||
|
items.length > 0 ? Math.min(...items.map((i) => i.totalAmount)) : 0,
|
||||||
|
maxAmount:
|
||||||
|
items.length > 0 ? Math.max(...items.map((i) => i.totalAmount)) : 0,
|
||||||
|
},
|
||||||
|
generatedAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildDateFilter(
|
||||||
|
startDate?: string,
|
||||||
|
endDate?: string,
|
||||||
|
type?: ReportType,
|
||||||
|
): any {
|
||||||
|
if (!startDate && !endDate) return {};
|
||||||
|
|
||||||
|
const filter: any = {};
|
||||||
|
const dateField = type === ReportType.TRANSACTION ? 'date' : 'startDate';
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
filter[dateField] = { $gte: new Date(startDate) };
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
const end = new Date(endDate);
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
filter[dateField] = { ...filter[dateField], $lte: end };
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGroupByField(period: ReportPeriod): any {
|
||||||
|
switch (period) {
|
||||||
|
case ReportPeriod.DAILY:
|
||||||
|
return '$day';
|
||||||
|
case ReportPeriod.WEEKLY:
|
||||||
|
return { year: '$year', week: '$week' };
|
||||||
|
case ReportPeriod.MONTHLY:
|
||||||
|
return { year: '$year', month: '$month' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatPeriod(value: any, period: ReportPeriod): string {
|
||||||
|
if (period === ReportPeriod.DAILY) return value;
|
||||||
|
if (period === ReportPeriod.WEEKLY) {
|
||||||
|
return `${value.year}-W${String(value.week).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
if (period === ReportPeriod.MONTHLY) {
|
||||||
|
return `${value.year}-${String(value.month).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MongodbModule } from '../mongodb/mongodb.module';
|
||||||
|
import { SyncService } from './sync.service';
|
||||||
|
import { SyncScheduler } from './sync.scheduler';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [MongodbModule],
|
||||||
|
providers: [SyncService, SyncScheduler],
|
||||||
|
exports: [SyncService],
|
||||||
|
})
|
||||||
|
export class SyncModule {}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { SyncService } from './sync.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SyncScheduler {
|
||||||
|
private readonly logger = new Logger(SyncScheduler.name);
|
||||||
|
|
||||||
|
constructor(private syncService: SyncService) {}
|
||||||
|
|
||||||
|
@Cron(CronExpression.EVERY_5_MINUTES)
|
||||||
|
async handleIncrementalSync() {
|
||||||
|
this.logger.log('⏰ Running scheduled incremental sync');
|
||||||
|
try {
|
||||||
|
await this.syncService.incrementalSync();
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
this.logger.error('❌ Scheduled sync failed', error.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { TransactionDoc } from '../mongodb/schemas/transaction.schema';
|
||||||
|
import { SubscriptionDoc } from '../mongodb/schemas/subscription.schema';
|
||||||
|
import { getYear, getMonth, getWeek, format } from 'date-fns';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SyncService {
|
||||||
|
private readonly logger = new Logger(SyncService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
@InjectModel(TransactionDoc.name)
|
||||||
|
private transactionModel: Model<TransactionDoc>,
|
||||||
|
@InjectModel(SubscriptionDoc.name)
|
||||||
|
private subscriptionModel: Model<SubscriptionDoc>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async syncTransactions(fromDate?: Date): Promise<void> {
|
||||||
|
this.logger.log('🔄 Starting transaction sync...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const whereClause = fromDate ? { updatedAt: { gte: fromDate } } : {};
|
||||||
|
|
||||||
|
const transactions = await this.prisma.payment.findMany({
|
||||||
|
where: {},
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`📊 Found ${transactions.length} transactions to sync`);
|
||||||
|
|
||||||
|
if (transactions.length === 0) {
|
||||||
|
this.logger.log('✅ No transactions to sync');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchSize = 100;
|
||||||
|
for (let i = 0; i < transactions.length; i += batchSize) {
|
||||||
|
const batch = transactions.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
const operations = batch.map((transaction) => {
|
||||||
|
const date = new Date(transaction.createdAt);
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateOne: {
|
||||||
|
filter: { transactionId: transaction.id },
|
||||||
|
update: {
|
||||||
|
$set: {
|
||||||
|
transactionId: transaction.id,
|
||||||
|
date: transaction.createdAt,
|
||||||
|
amount: transaction.amount,
|
||||||
|
//todo add tax field in prisma payment model
|
||||||
|
tax: transaction.amount,
|
||||||
|
status: transaction.status,
|
||||||
|
merchantPartnerId: transaction.merchantPartnerId,
|
||||||
|
year: getYear(date),
|
||||||
|
month: getMonth(date) + 1,
|
||||||
|
week: getWeek(date),
|
||||||
|
day: format(date, 'yyyy-MM-dd'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
upsert: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.transactionModel.bulkWrite(operations);
|
||||||
|
this.logger.log(
|
||||||
|
`✅ Synced ${Math.min(i + batchSize, transactions.length)}/${transactions.length}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('✅ Transaction sync completed');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Error syncing transactions', error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncSubscriptions(fromDate?: Date): Promise<void> {
|
||||||
|
this.logger.log('🔄 Starting subscription sync...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const whereClause = fromDate ? { updatedAt: { gte: fromDate } } : {};
|
||||||
|
|
||||||
|
const subscriptions = await this.prisma.subscription.findMany({
|
||||||
|
where: whereClause,
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`📊 Found ${subscriptions.length} subscriptions to sync`);
|
||||||
|
|
||||||
|
if (subscriptions.length === 0) {
|
||||||
|
this.logger.log('✅ No subscriptions to sync');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchSize = 100;
|
||||||
|
for (let i = 0; i < subscriptions.length; i += batchSize) {
|
||||||
|
const batch = subscriptions.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
const operations = batch.map((subscription) => {
|
||||||
|
const date = new Date(subscription.startDate);
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateOne: {
|
||||||
|
filter: { subscriptionId: subscription.id },
|
||||||
|
update: {
|
||||||
|
$set: {
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
startDate: subscription.startDate,
|
||||||
|
endDate: subscription.endDate,
|
||||||
|
amount: subscription.amount,
|
||||||
|
currency: subscription.currency,
|
||||||
|
status: subscription.status,
|
||||||
|
periodicity: subscription.periodicity,
|
||||||
|
merchantPartnerId: subscription.merchantPartnerId,
|
||||||
|
year: getYear(date),
|
||||||
|
month: getMonth(date) + 1,
|
||||||
|
week: getWeek(date),
|
||||||
|
day: format(date, 'yyyy-MM-dd'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
upsert: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.subscriptionModel.bulkWrite(operations);
|
||||||
|
this.logger.log(
|
||||||
|
`✅ Synced ${Math.min(i + batchSize, subscriptions.length)}/${subscriptions.length}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('✅ Subscription sync completed');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Error syncing subscriptions', error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fullSync(): Promise<void> {
|
||||||
|
this.logger.log('🚀 Starting full sync...');
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
await this.syncTransactions();
|
||||||
|
await this.syncSubscriptions();
|
||||||
|
|
||||||
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||||
|
this.logger.log(`🎉 Full sync completed in ${duration}s`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async incrementalSync(): Promise<void> {
|
||||||
|
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
||||||
|
this.logger.log(`🔄 Starting incremental sync...`);
|
||||||
|
|
||||||
|
await this.syncTransactions(fiveMinutesAgo);
|
||||||
|
await this.syncSubscriptions(fiveMinutesAgo);
|
||||||
|
|
||||||
|
this.logger.log('✅ Incremental sync completed');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user