Skip to content

Commit 65450ce

Browse files
authored
hot-update: add confirm and result service (#67)
This pull request introduces a new feature for handling application payment evidence, including database schema changes, new module integration, and API endpoints. It also refactors some existing guards and DTOs for improved clarity and correctness. The most important changes are grouped below: **Payment Evidence Feature Integration** * Added the `ApplicationPaymentEvidence` model to `prisma/schema.prisma` and corresponding migration, including relations to `ApplicationFile` and `StudentApplication` and new fields for payment details. [[1]](diffhunk://#diff-5b443964f4f3a611682db8f7e02177b0a8c632b2039e2bd5e4dd7347815c565cR25-R56) [[2]](diffhunk://#diff-5b443964f4f3a611682db8f7e02177b0a8c632b2039e2bd5e4dd7347815c565cR159-R160) [[3]](diffhunk://#diff-5b443964f4f3a611682db8f7e02177b0a8c632b2039e2bd5e4dd7347815c565cR340-R341) [[4]](diffhunk://#diff-b1bfc9c3a4233a21db170e15c39c55027003c5037182b38e3ea66b76018bfc05R1-R32) * Introduced the `ApplicationPaymentEvidenceModule`, controller, service, DTO, and types for verifying bank slips, including API endpoint `/api/application/payment-evidence/upload` with file validation and stubbed logic for slip verification. [[1]](diffhunk://#diff-089f4f2474b64391c42b6e66aed33977e132058d92108f0a63234a7862e1f8b8R23) [[2]](diffhunk://#diff-089f4f2474b64391c42b6e66aed33977e132058d92108f0a63234a7862e1f8b8R93) [[3]](diffhunk://#diff-4ef0a476aa1e31d1b3206096eb0e970fd2af11e376e3aaa2c0af2ded679d318eR1-R97) [[4]](diffhunk://#diff-985549e5c9228c6b1ac8358d2ec77c51acc7c5f6a28406237a8f587556aee772R1-R29) [[5]](diffhunk://#diff-a35fd0ab81ffbac22de2874e1f05d690305f47f7c8629195016e005e8fe0233fR1-R11) [[6]](diffhunk://#diff-345a1edb7b7bf627f21d29b08856bc426eba54457b1eefa5f4461e4aae37889fR1-R137) [[7]](diffhunk://#diff-b5dd2cd364a4b7c9c838c81ca8af220c73382ddce53f0ff96d50816dce772bfaR1-R6) **API and Guard Improvements** * Added `ApplicationPassGuard` to restrict payment evidence upload to users with passed and staff-allowed applications. * Refactored `AnnouncePeriodGuard` to `AnnounceAndConfirmPeriodGuard` and updated its usage across controllers for clarity. [[1]](diffhunk://#diff-b229fbd3353d6fac838a965103a6fd141764b11211244fe15edae38db6e6ab2dL10-R10) [[2]](diffhunk://#diff-279a2a4d44bc0c2b500f1bc2ad9fac62b393b5c9fc7fe2bc56ef12062fe381cfL4-R4) [[3]](diffhunk://#diff-279a2a4d44bc0c2b500f1bc2ad9fac62b393b5c9fc7fe2bc56ef12062fe381cfL63-R63) [[4]](diffhunk://#diff-34558e215c546c8fe49eaf7b2355ef614e90378a75ce2d8b3faf13e0006fe037L3-R3) **DTO and Controller Updates** * Updated `AllowToConfirmDto` to use `allow` instead of `confirm` and added `IsNotEmpty` validation for correctness. * Fixed `StaffStatusController` to return the result of `changeResult` for proper API response. **Configuration Changes** * Added `apis.slipKey` to `app.config.ts` for external slip verification API integration. **Documentation** * Adjusted DB-Diagram iframe width in `README.md` for improved layout.
2 parents f093516 + 4d46819 commit 65450ce

19 files changed

Lines changed: 415 additions & 13 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
</ul>
6262

6363
<h3>DB-Diagram (Prototype)</h3>
64-
<iframe width="560" height="315" src='https://dbdiagram.io/e/69ae7fb9cf54053b6f3980fa/69ae7fffcf54053b6f398582'> </iframe>
64+
<iframe width="500" height="315" src='https://dbdiagram.io/e/69ae7fb9cf54053b6f3980fa/69ae7fffcf54053b6f398582'> </iframe>
6565

6666
<h3>API Flow Design (Prototype)</h3>
6767
<a href="https://www.figma.com/board/ZO9E1iaCZasX5wwyK0D6g9/CC37-Backend-Routing-Flow?node-id=0-1&t=meCqyS4DR1seJfqG-1"><p>Open on Figma</p></a>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
-- CreateTable
2+
CREATE TABLE "ApplicationPaymentEvidence" (
3+
"pe_id" TEXT NOT NULL,
4+
"pe_transaction_ref" TEXT NOT NULL,
5+
"pe_transaction_date" TIMESTAMP(3) NOT NULL,
6+
"pe_transaction_expect_amount" DOUBLE PRECISION NOT NULL DEFAULT 500.00,
7+
"pe_transaction_actual_amount" DOUBLE PRECISION NOT NULL,
8+
"pe_json" TEXT NOT NULL,
9+
"pe_sender_account_name" TEXT NOT NULL,
10+
"pe_sender_account_number" TEXT NOT NULL,
11+
"pe_sender_bank_id" TEXT NOT NULL,
12+
"pe_sender_bank_name" TEXT NOT NULL,
13+
"pe_reciever_account_name" TEXT NOT NULL,
14+
"pe_reciever_account_number" TEXT NOT NULL,
15+
"pe_reciever_bank_id" TEXT NOT NULL,
16+
"pe_reciever_bank_name" TEXT NOT NULL,
17+
"std_file_key" TEXT NOT NULL,
18+
"std_application_id" TEXT NOT NULL,
19+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
20+
"updated_at" TIMESTAMP(3) NOT NULL,
21+
22+
CONSTRAINT "ApplicationPaymentEvidence_pkey" PRIMARY KEY ("pe_id")
23+
);
24+
25+
-- CreateIndex
26+
CREATE UNIQUE INDEX "ApplicationPaymentEvidence_std_file_key_key" ON "ApplicationPaymentEvidence"("std_file_key");
27+
28+
-- AddForeignKey
29+
ALTER TABLE "ApplicationPaymentEvidence" ADD CONSTRAINT "ApplicationPaymentEvidence_std_file_key_fkey" FOREIGN KEY ("std_file_key") REFERENCES "ApplicationFile"("std_file_key") ON DELETE RESTRICT ON UPDATE CASCADE;
30+
31+
-- AddForeignKey
32+
ALTER TABLE "ApplicationPaymentEvidence" ADD CONSTRAINT "ApplicationPaymentEvidence_std_application_id_fkey" FOREIGN KEY ("std_application_id") REFERENCES "StudentApplication"("std_application_id") ON DELETE RESTRICT ON UPDATE CASCADE;

prisma/schema.prisma

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,38 @@ model ApplicationTotalScore {
2222
updated_at DateTime @updatedAt
2323
}
2424

25+
model ApplicationPaymentEvidence {
26+
pe_id String @id @default(uuid())
27+
pe_transaction_ref String
28+
pe_transaction_date DateTime
29+
30+
pe_transaction_expect_amount Float @default(500.00)
31+
pe_transaction_actual_amount Float
32+
33+
pe_json String @db.Text
34+
35+
pe_sender_account_name String
36+
pe_sender_account_number String
37+
38+
pe_sender_bank_id String
39+
pe_sender_bank_name String
40+
41+
pe_reciever_account_name String
42+
pe_reciever_account_number String
43+
44+
pe_reciever_bank_id String
45+
pe_reciever_bank_name String
46+
47+
std_file_key String @unique
48+
std_file ApplicationFile @relation(fields: [std_file_key], references: [std_file_key])
49+
50+
std_application_id String
51+
std_application StudentApplication @relation(fields: [std_application_id], references: [std_application_id])
52+
53+
created_at DateTime @default(now())
54+
updated_at DateTime @updatedAt
55+
}
56+
2557
model StaffEmailHistory {
2658
email_id String @default(uuid()) @id
2759
@@ -123,6 +155,8 @@ model ApplicationFile {
123155
std_file_size Int?
124156
std_file_key String @id
125157
std_file_type FileType
158+
159+
pe_payment_evidence ApplicationPaymentEvidence?
126160
127161
std_file_disabled Boolean @default(false)
128162
@@ -303,6 +337,8 @@ model StudentApplication {
303337
304338
std_status ApplicationStatus?
305339
340+
pe_payment_evidence ApplicationPaymentEvidence[]
341+
306342
created_at DateTime @default(now())
307343
updated_at DateTime @updatedAt
308344
}

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { auth } from "./lib/auth";
2020
import { ApplicationConfirmationModule } from "./modules/application-confirmation/application-confirmation.module";
2121
import { ApplicationFileModule } from "./modules/application-file/application-file.module";
2222
import { ApplicationInfoModule } from "./modules/application-info/application-info.module";
23+
import { ApplicationPaymentEvidenceModule } from "./modules/application-payment-evidence/application-payment-evidence.module";
2324
import { ApplicationQuestionModule } from "./modules/application-question/application-question.module";
2425
import { ApplicationStatusModule } from "./modules/application-status/application-status.module";
2526
import { ApplicationSubmitModule } from "./modules/application-submit/application-submit.module";
@@ -89,6 +90,7 @@ import { UtilModule } from "./modules/util/util.module";
8990
StaffEmailModule,
9091
StaffTotalScoreModule,
9192
StaffLeaderboardModule,
93+
ApplicationPaymentEvidenceModule,
9294
],
9395

9496
controllers: [AppController],

src/common/guards/announce-period.guard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { config } from "src/config/app.config";
77
import { PrismaService } from "src/core/prisma/prisma.service";
88

99
@Injectable()
10-
export class AnnouncePeriodGuard implements CanActivate {
10+
export class AnnounceAndConfirmPeriodGuard implements CanActivate {
1111
canActivate(context: ExecutionContext): boolean {
1212
try {
1313
if (config.resultAnnounceAndConfirmPeriod.bypass) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Type } from "@aws-sdk/client-s3";
2+
import { CanActivate, ExecutionContext, ForbiddenException, Injectable, InternalServerErrorException } from "@nestjs/common";
3+
import type { UserSession } from "@thallesp/nestjs-better-auth";
4+
import { Session } from "@thallesp/nestjs-better-auth";
5+
import { Observable, retry } from "rxjs";
6+
import { PrismaService } from "src/core/prisma/prisma.service";
7+
8+
@Injectable()
9+
export class ApplicationPassGuard implements CanActivate {
10+
constructor(private readonly prisma: PrismaService) {}
11+
12+
async canActivate(context: ExecutionContext): Promise<boolean> {
13+
const request = context.switchToHttp().getRequest();
14+
const session = request.session as UserSession;
15+
16+
try {
17+
const studentApplication = await this.prisma.studentApplication.findMany({
18+
where: {
19+
std_user: {
20+
id: session.user.id,
21+
},
22+
},
23+
});
24+
25+
if (studentApplication.length > 0) {
26+
throw "No application found linked with your account";
27+
}
28+
29+
const filterPassApplication = studentApplication.filter((app) => app.std_application_result === "pass");
30+
31+
if (filterPassApplication.length <= 0) {
32+
throw `Your Application in : ${studentApplication.map((app) => app.std_application_result).join(", ")} stage`;
33+
}
34+
35+
const filterAllowConfirm = filterPassApplication.filter((app) => app.stf_application_allow_confirm === true);
36+
37+
if (filterAllowConfirm.length <= 0) {
38+
throw `Please wait staff to allow you to confirm`;
39+
}
40+
41+
return true;
42+
} catch (e) {
43+
throw new ForbiddenException(e);
44+
}
45+
}
46+
}

src/config/app.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@ export const config = {
5252
from: process.env.MAIL_FROM || "",
5353
},
5454
},
55+
apis: {
56+
slipKey: process.env.API_EASYSLIP_KEY,
57+
},
5558
} as const;

src/modules/application-confirmation/application-confirmation.controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Body, Controller, Get, Param, Post, UseGuards } from "@nestjs/common";
22
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from "@nestjs/swagger";
33
import { Session, type UserSession } from "@thallesp/nestjs-better-auth";
4-
import { AnnouncePeriodGuard } from "src/common/guards/announce-period.guard";
4+
import { AnnounceAndConfirmPeriodGuard } from "src/common/guards/announce-period.guard";
55
import { RegisterPeriodGuard } from "src/common/guards/register-period.guard";
66
import { ApplicationConfirmationService } from "./application-confirmation.service";
77
import { ApplicationConfirmationDto } from "./dto/application-confirmation.dto";
@@ -60,7 +60,7 @@ export class ApplicationConfirmationController {
6060
}
6161

6262
@Post("/")
63-
@UseGuards(AnnouncePeriodGuard)
63+
@UseGuards(AnnounceAndConfirmPeriodGuard)
6464
@ApiOperation({
6565
description: "Confirm or decline an application. Requires application to be passed and allowed to confirm.",
6666
})
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Request Types
2+
export interface VerifyBankRequest {
3+
// One of the following is required
4+
payload?: string; // QR payload (1-128 chars)
5+
image?: File; // Image file (multipart/form-data)
6+
base64?: string; // Base64 encoded image
7+
url?: string; // URL to image (1-2048 chars)
8+
9+
// Optional parameters
10+
remark?: string; // 1-255 chars
11+
matchAccount?: boolean;
12+
matchAmount?: number;
13+
checkDuplicate?: boolean;
14+
}
15+
16+
// Response Types
17+
export interface VerifyBankResponse {
18+
success: true;
19+
data: VerifyBankData;
20+
message: string;
21+
}
22+
23+
export interface VerifyBankData {
24+
remark?: string;
25+
isDuplicate: boolean;
26+
matchedAccount: MatchedAccount | null;
27+
amountInOrder?: number;
28+
amountInSlip: number;
29+
isAmountMatched?: boolean;
30+
rawSlip: RawSlip;
31+
}
32+
33+
export interface MatchedAccount {
34+
bank: {
35+
nameTh: string;
36+
nameEn: string;
37+
code: string;
38+
shortCode: string;
39+
};
40+
nameTh: string;
41+
nameEn: string;
42+
type: "PERSONAL" | "JURISTIC";
43+
bankNumber: string;
44+
}
45+
46+
export interface RawSlip {
47+
payload: string;
48+
transRef: string;
49+
date: string; // ISO 8601
50+
countryCode: string;
51+
amount: Amount;
52+
fee: number;
53+
ref1: string;
54+
ref2: string;
55+
ref3: string;
56+
sender: Party;
57+
receiver: Party & { merchantId?: string | null };
58+
}
59+
60+
export interface Amount {
61+
amount: number;
62+
local: {
63+
amount: number;
64+
currency: string;
65+
};
66+
}
67+
68+
export interface Party {
69+
bank: {
70+
id: string;
71+
name: string;
72+
short: string;
73+
};
74+
account: {
75+
name: {
76+
th?: string;
77+
en?: string;
78+
};
79+
bank?: {
80+
type: "BANKAC" | "TOKEN" | "DUMMY";
81+
account: string;
82+
};
83+
proxy?: {
84+
type: "NATID" | "MSISDN" | "EWALLETID" | "EMAIL" | "BILLERID";
85+
account: string;
86+
};
87+
};
88+
}
89+
90+
// Error Response
91+
export interface ErrorResponse {
92+
success: false;
93+
error: {
94+
code: string;
95+
message: string;
96+
};
97+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Body, Controller, FileTypeValidator, MaxFileSizeValidator, ParseFilePipe, Post, UploadedFile, UseGuards, UseInterceptors } from "@nestjs/common";
2+
import { FileInterceptor } from "@nestjs/platform-express";
3+
import { Session, type UserSession } from "@thallesp/nestjs-better-auth";
4+
import { AnnounceAndConfirmPeriodGuard } from "src/common/guards/announce-period.guard";
5+
import { ApplicationPassGuard } from "src/common/guards/application-pass.guard";
6+
import { ApplicationPaymentEvidenceService } from "./application-payment-evidence.service";
7+
import { ApplicationPaymentEvidenceDto } from "./dto/application-payment-evidence.dto";
8+
9+
@Controller("/api/application/payment-evidence")
10+
export class ApplicationPaymentEvidenceController {
11+
constructor(private readonly applicationPaymentEvidenceService: ApplicationPaymentEvidenceService) {}
12+
13+
@Post("/upload")
14+
// @UseGuards(AnnounceAndConfirmPeriodGuard)
15+
// @UseGuards(ApplicationPassGuard)
16+
@UseInterceptors(FileInterceptor("file"))
17+
uploadEvidence(
18+
@Session() session: UserSession,
19+
@Body() applicationPaymentEvidenceDto: ApplicationPaymentEvidenceDto,
20+
@UploadedFile(
21+
new ParseFilePipe({
22+
validators: [new MaxFileSizeValidator({ maxSize: 3 * 1024 * 1024 }), new FileTypeValidator({ fileType: "image/*" })],
23+
}),
24+
)
25+
file: Express.Multer.File,
26+
) {
27+
return this.applicationPaymentEvidenceService.uploadEvidence(session.user.id, applicationPaymentEvidenceDto, file);
28+
}
29+
}

0 commit comments

Comments
 (0)