Skip to content

PM-921 - implement missing methods & checks for winnings & admin winn… #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 85 additions & 25 deletions src/api/admin-winning/adminWinning.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
PaymentStatus,
AuditPayoutDto,
} from 'src/dto/adminWinning.dto';
import { TaxFormRepository } from '../repository/taxForm.repo';
import { PaymentMethodRepository } from '../repository/paymentMethod.repo';

const ONE_DAY = 24 * 60 * 60 * 1000;

Expand All @@ -30,7 +32,11 @@ export class AdminWinningService {
* Constructs the admin winning service with the given dependencies.
* @param prisma the prisma service.
*/
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly taxFormRepo: TaxFormRepository,
private readonly paymentMethodRepo: PaymentMethodRepository,
) {}

/**
* Search winnings with parameters
Expand Down Expand Up @@ -101,7 +107,7 @@ export class AdminWinningService {
details: item.payment?.map((paymentItem) => ({
id: paymentItem.payment_id,
netAmount: Number(paymentItem.net_amount),
grossAmount: Number(paymentItem.gross_amount),
grossAmount: (paymentItem.gross_amount),
totalAmount: Number(paymentItem.total_amount),
installmentNumber: paymentItem.installment_number,
datePaid: paymentItem.date_paid ?? undefined,
Expand Down Expand Up @@ -276,6 +282,21 @@ export class AdminWinningService {
return orderBy;
}

private getPaymentsByWinningsId(winningsId: string, paymentId?: string) {
return this.prisma.payment.findMany({
where: {
winnings_id: {
equals: winningsId,
},
payment_id: paymentId
? {
equals: paymentId,
}
: undefined,
},
});
}

/**
* Update winnings with parameters
* @param body the request body
Expand All @@ -291,18 +312,10 @@ export class AdminWinningService {
let needsReconciliation = false;
const winningsId = body.winningsId;
try {
const payments = await this.prisma.payment.findMany({
where: {
winnings_id: {
equals: winningsId,
},
payment_id: body.paymentId
? {
equals: body.paymentId,
}
: undefined,
},
});
const payments = await this.getPaymentsByWinningsId(
winningsId,
body.paymentId,
);

if (payments.length === 0) {
throw new NotFoundException('failed to get current payments');
Expand All @@ -316,6 +329,13 @@ export class AdminWinningService {
const transactions: PrismaPromise<any>[] = [];
const now = new Date().getTime();
payments.forEach((payment) => {
if (
payment.payment_status &&
payment.payment_status === PaymentStatus.CANCELLED
) {
throw new BadRequestException('cannot update cancelled winnings');
}

let version = payment.version ?? 1;
// Update Payment Status if requested
if (body.paymentStatus) {
Expand Down Expand Up @@ -356,7 +376,10 @@ export class AdminWinningService {
break;
}

if (errMessage) {
if (
errMessage &&
payment.payment_status === PaymentStatus.PROCESSING
) {
throw new BadRequestException(errMessage);
}

Expand All @@ -371,7 +394,10 @@ export class AdminWinningService {
),
);
version += 1;
needsReconciliation = true;

if (body.paymentStatus === PaymentStatus.OWED) {
needsReconciliation = true;
}

if (payment.installment_number === 1) {
transactions.push(
Expand All @@ -388,11 +414,6 @@ export class AdminWinningService {
// Update Release Date if requested
if (body.releaseDate) {
const newReleaseDate = new Date(body.releaseDate);
if (newReleaseDate.getTime() < now) {
throw new BadRequestException(
'new release date cannot be in the past',
);
}

transactions.push(
this.updateReleaseDate(
Expand All @@ -417,10 +438,11 @@ export class AdminWinningService {
}
}

// Update Release Date if requested
// Update payment amount if requested
if (
body.paymentAmount !== undefined &&
(payment.payment_status === PaymentStatus.OWED ||
payment.payment_status === PaymentStatus.ON_HOLD ||
payment.payment_status === PaymentStatus.ON_HOLD ||
payment.payment_status === PaymentStatus.ON_HOLD_ADMIN)
) {
Expand Down Expand Up @@ -661,9 +683,47 @@ export class AdminWinningService {
});
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async reconcileWinningsStatusOnUserDetailsUpdate(userId: string) {
// not implement, because it's about detail payment
/**
* Update payment for user from one status to another
*
* @param userId user id
* @param fromStatus from status
* @param toStatus to status
* @param tx transaction
*/
updateWinningsStatus(userId, fromStatus, toStatus) {
return this.prisma.$executeRaw`
UPDATE payment
SET payment_status = ${toStatus}::payment_status,
updated_at = now(),
updated_by = 'system',
version = version + 1
FROM winnings
WHERE payment.winnings_id = winnings.winning_id
AND winnings.winner_id = ${userId}
AND payment.payment_status = ${fromStatus}::payment_status AND version = version
`;
}

/**
* Reconcile winning if user data updated
*
* @param userId user id
*/
async reconcileWinningsStatusOnUserDetailsUpdate(userId) {
const hasTaxForm = await this.taxFormRepo.hasActiveTaxForm(userId);
const hasPaymentMethod =
await this.paymentMethodRepo.hasVerifiedPaymentMethod(userId);
let fromStatus, toStatus;
if (hasTaxForm && hasPaymentMethod) {
fromStatus = PaymentStatus.ON_HOLD;
toStatus = PaymentStatus.OWED;
} else {
fromStatus = PaymentStatus.OWED;
toStatus = PaymentStatus.ON_HOLD;
}

await this.updateWinningsStatus(userId, fromStatus, toStatus);
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { APP_GUARD } from '@nestjs/core';
import { TokenValidatorMiddleware } from 'src/core/auth/middleware/tokenValidator.middleware';
import { AuthGuard, RolesGuard } from 'src/core/auth/guards';
import { TopcoderModule } from 'src/shared/topcoder/topcoder.module';
import { OriginRepository } from './repository/origin.repo';
import { TaxFormRepository } from './repository/taxForm.repo';
import { PaymentMethodRepository } from './repository/paymentMethod.repo';

@Module({
imports: [GlobalProvidersModule, TopcoderModule],
Expand All @@ -34,6 +37,9 @@ import { TopcoderModule } from 'src/shared/topcoder/topcoder.module';
AdminWinningService,
WinningService,
WalletService,
OriginRepository,
TaxFormRepository,
PaymentMethodRepository,
],
})
export class ApiModule implements NestModule {
Expand Down
24 changes: 24 additions & 0 deletions src/api/repository/origin.repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/shared/global/prisma.service';

@Injectable()
export class OriginRepository {
constructor(private readonly prisma: PrismaService) {}

/**
* Get origin id by name
*
* @param name origin name
* @param tx transaction
*/
async getOriginIdByName(name: string, tx?): Promise<number | null> {
const db = tx || this.prisma;
const originData = await db.origin.findFirst({
where: { origin_name: name },
});
if (!originData) {
return null;
}
return originData.origin_id as number;
}
}
49 changes: 49 additions & 0 deletions src/api/repository/paymentMethod.repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import {
PaymentMethodQueryResult,
UserPaymentMethodStatus,
} from 'src/dto/paymentMethod.dto';
import { PrismaService } from 'src/shared/global/prisma.service';

@Injectable()
export class PaymentMethodRepository {
constructor(private readonly prisma: PrismaService) {}

/**
* Check user has verified payment method
*
* @param userId user id
* @param tx transaction
*/
async hasVerifiedPaymentMethod(userId: string, tx?): Promise<boolean> {
const methods = await this.findPaymentMethodByUserId(userId, tx);
for (const method of methods) {
if (
method.status ===
UserPaymentMethodStatus.UserPaymentMethodStatusConnected.toString()
) {
return true;
}
}
return false;
}

/**
* Get user payment methods
*
* @param userId user id
* @param tx transaction
* @returns payment methods
*/
private async findPaymentMethodByUserId(userId: string, tx?) {
const query = `
SELECT pm.payment_method_id, pm.payment_method_type, pm.name, pm.description, upm.status, upm.id
FROM payment_method pm
JOIN user_payment_methods upm ON pm.payment_method_id = upm.payment_method_id
WHERE upm.user_id = '${userId}'
`;
const db = tx || this.prisma;
const ret = await db.$queryRawUnsafe(query);
return (ret || []) as PaymentMethodQueryResult[];
}
}
48 changes: 48 additions & 0 deletions src/api/repository/taxForm.repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import { TaxFormStatus } from 'src/dto/adminWinning.dto';
import { TaxFormQueryResult } from 'src/dto/taxForm.dto';
import { PrismaService } from 'src/shared/global/prisma.service';

@Injectable()
export class TaxFormRepository {
constructor(private readonly prisma: PrismaService) {}

/**
* Check user has tax form or not
*
* @param userId user id
* @returns true if user has active tax form
*/
async hasActiveTaxForm(userId: string): Promise<boolean> {
const ret = await this.findTaxFormByUserId(userId);
for (const r of ret) {
if (r.status_id === TaxFormStatus.Active.toString()) {
return true;
}
}
return false;
}

/**
* Find tax forms by user id
*
* @param userId user id
* @param tx transaction
* @returns tax forms
*/
async findTaxFormByUserId(
userId: string,
tx?,
): Promise<TaxFormQueryResult[]> {
const query = `
SELECT u.id, u.user_id, t.tax_form_id, t.name, t.text, t.description, u.date_filed, u.withholding_amount, u.withholding_percentage, u.status_id::text, u.use_percentage
FROM user_tax_form_associations AS u
JOIN tax_forms AS t ON u.tax_form_id = t.tax_form_id
WHERE u.user_id = '${userId}'
`;
const db = tx || this.prisma;

const ret = await db.$queryRawUnsafe(query);
return (ret || []) as TaxFormQueryResult[];
}
}
7 changes: 3 additions & 4 deletions src/api/winning/winning.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,10 @@ export class WinningController {
@Body() body: WinningRequestDto,
): Promise<ResponseDto<SearchWinningResult>> {
const result = await this.adminWinningService.searchWinnings(body);
if (result.error) {
result.status = ResponseStatusType.ERROR;
}

result.status = ResponseStatusType.SUCCESS;
result.status = result.error
? ResponseStatusType.ERROR
: ResponseStatusType.SUCCESS;

return result;
}
Expand Down
Loading