Skip to content

Commit 4d46819

Browse files
authored
hot-update: confirm service (#66)
This pull request introduces a new feature for handling payment evidence uploads and verification for student applications, along with several related database and codebase changes. The most important changes include the addition of a new `ApplicationPaymentEvidence` model and migration, new API endpoints and services for uploading and verifying payment evidence, updates to guards and DTOs, and integration with external slip verification APIs. **Payment Evidence Feature** * Added new `ApplicationPaymentEvidence` model to `prisma/schema.prisma` and corresponding migration, including fields for transaction details, sender/receiver info, and relations to `ApplicationFile` and `StudentApplication`. [[1]](diffhunk://#diff-5b443964f4f3a611682db8f7e02177b0a8c632b2039e2bd5e4dd7347815c565cR25-R56) [[2]](diffhunk://#diff-b1bfc9c3a4233a21db170e15c39c55027003c5037182b38e3ea66b76018bfc05R1-R32) * Updated `ApplicationFile` and `StudentApplication` models to reference `ApplicationPaymentEvidence`, enabling linkage between files, applications, and payment evidence. [[1]](diffhunk://#diff-5b443964f4f3a611682db8f7e02177b0a8c632b2039e2bd5e4dd7347815c565cR159-R160) [[2]](diffhunk://#diff-5b443964f4f3a611682db8f7e02177b0a8c632b2039e2bd5e4dd7347815c565cR340-R341) **API & Service Implementation** * Introduced `ApplicationPaymentEvidenceModule`, controller, service, and DTOs for handling evidence uploads, including file validation and integration with external slip verification APIs. [[1]](diffhunk://#diff-089f4f2474b64391c42b6e66aed33977e132058d92108f0a63234a7862e1f8b8R23) [[2]](diffhunk://#diff-089f4f2474b64391c42b6e66aed33977e132058d92108f0a63234a7862e1f8b8R93) [[3]](diffhunk://#diff-a35fd0ab81ffbac22de2874e1f05d690305f47f7c8629195016e005e8fe0233fR1-R11) [[4]](diffhunk://#diff-985549e5c9228c6b1ac8358d2ec77c51acc7c5f6a28406237a8f587556aee772R1-R29) [[5]](diffhunk://#diff-345a1edb7b7bf627f21d29b08856bc426eba54457b1eefa5f4461e4aae37889fR1-R137) [[6]](diffhunk://#diff-b5dd2cd364a4b7c9c838c81ca8af220c73382ddce53f0ff96d50816dce772bfaR1-R6) [[7]](diffhunk://#diff-4ef0a476aa1e31d1b3206096eb0e970fd2af11e376e3aaa2c0af2ded679d318eR1-R97) * Added new guard `ApplicationPassGuard` to ensure only eligible applications can upload payment evidence. **Guard & Controller Updates** * Renamed and updated guards and controller usages from `AnnouncePeriodGuard` to `AnnounceAndConfirmPeriodGuard` for improved clarity and coverage of confirmation periods. [[1]](diffhunk://#diff-b229fbd3353d6fac838a965103a6fd141764b11211244fe15edae38db6e6ab2dL10-R10) [[2]](diffhunk://#diff-279a2a4d44bc0c2b500f1bc2ad9fac62b393b5c9fc7fe2bc56ef12062fe381cfL4-R4) [[3]](diffhunk://#diff-279a2a4d44bc0c2b500f1bc2ad9fac62b393b5c9fc7fe2bc56ef12062fe381cfL63-R63) [[4]](diffhunk://#diff-34558e215c546c8fe49eaf7b2355ef614e90378a75ce2d8b3faf13e0006fe037L3-R3) **Configuration & Validation** * Added external API keys to configuration for slip verification integration. * Updated DTOs for staff status and confirmation to improve validation and naming consistency. [[1]](diffhunk://#diff-739bf614d12f2b84c455b0a31584cb09207cd50708f4ffd3c6e3260bbbbda4a5L2-R2) [[2]](diffhunk://#diff-739bf614d12f2b84c455b0a31584cb09207cd50708f4ffd3c6e3260bbbbda4a5R43-R45) **Miscellaneous** * Minor UI update: Adjusted DB diagram iframe width in `README.md`. * Fixed controller to return result for change-result endpoint in staff status.
2 parents ec43ee0 + f8dd13c commit 4d46819

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)