Skip to content

Commit 6d07c7b

Browse files
authored
Feat/slip verify (#68)
This pull request introduces significant improvements to the payment evidence upload and verification flow, with updates to the database schema, backend logic, and configuration for payment processing. The most important changes are grouped below by theme: **Database schema and validation improvements:** * Changed the primary key of the `ApplicationPaymentEvidence` table to `pe_transaction_ref`, removed the `pe_id` column, and added a required, unique `pe_transaction_payload` column to ensure evidence uniqueness and integrity. [[1]](diffhunk://#diff-6c815c00188c259e2527ddaeefcda7eac129dec344d5992fe276596f3bca06fdR1-R17) [[2]](diffhunk://#diff-5b443964f4f3a611682db8f7e02177b0a8c632b2039e2bd5e4dd7347815c565cL26-R27) **Payment evidence upload and verification logic:** * Refactored the payment evidence upload logic in `ApplicationPaymentEvidenceService` to use the EasySlip API for slip verification, validate receiver/account details, check for duplicate evidence, and create related records in S3 and the database. Additional validation ensures the payment matches the expected amount and updates application status accordingly. [[1]](diffhunk://#diff-345a1edb7b7bf627f21d29b08856bc426eba54457b1eefa5f4461e4aae37889fL2-L21) [[2]](diffhunk://#diff-345a1edb7b7bf627f21d29b08856bc426eba54457b1eefa5f4461e4aae37889fL38-R184) [[3]](diffhunk://#diff-345a1edb7b7bf627f21d29b08856bc426eba54457b1eefa5f4461e4aae37889fL87-R228) * Enabled guards (`AnnounceAndConfirmPeriodGuard`, `ApplicationPassGuard`) on the payment evidence upload endpoint to enforce application stage restrictions. * Improved error messaging in `ApplicationPassGuard` for clearer feedback when an application is not in the "pass" stage. **Configuration and dependency updates:** * Added a `payment` section to the application config, specifying bypass mode, receiver details, account numbers, and expected amount for payment validation. * Exported `ApplicationConfirmationService` from its module and injected it into `ApplicationPaymentEvidenceService` for confirmation updates after successful payment evidence upload. [[1]](diffhunk://#diff-9a9d69e42c0affd01f65d59b7a30e4ceba4798d7fe93e90a31d60ebf6f69b217R8) [[2]](diffhunk://#diff-a35fd0ab81ffbac22de2874e1f05d690305f47f7c8629195016e005e8fe0233fR3-R8) **Other improvements:** * Updated `ApplicationStatusService` to include related application data in status queries. * Added a test script `test/getTravelplan.ts` for querying top-scoring student applications with specific filters and relationships.
2 parents f05447e + 6e38dcb commit 6d07c7b

10 files changed

Lines changed: 283 additions & 86 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
Warnings:
3+
4+
- The primary key for the `ApplicationPaymentEvidence` table will be changed. If it partially fails, the table could be left without primary key constraint.
5+
- You are about to drop the column `pe_id` on the `ApplicationPaymentEvidence` table. All the data in the column will be lost.
6+
- A unique constraint covering the columns `[pe_transaction_payload]` on the table `ApplicationPaymentEvidence` will be added. If there are existing duplicate values, this will fail.
7+
- Added the required column `pe_transaction_payload` to the `ApplicationPaymentEvidence` table without a default value. This is not possible if the table is not empty.
8+
9+
*/
10+
-- AlterTable
11+
ALTER TABLE "ApplicationPaymentEvidence" DROP CONSTRAINT "ApplicationPaymentEvidence_pkey",
12+
DROP COLUMN "pe_id",
13+
ADD COLUMN "pe_transaction_payload" TEXT NOT NULL,
14+
ADD CONSTRAINT "ApplicationPaymentEvidence_pkey" PRIMARY KEY ("pe_transaction_ref");
15+
16+
-- CreateIndex
17+
CREATE UNIQUE INDEX "ApplicationPaymentEvidence_pe_transaction_payload_key" ON "ApplicationPaymentEvidence"("pe_transaction_payload");

prisma/schema.prisma

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ model ApplicationTotalScore {
2323
}
2424

2525
model ApplicationPaymentEvidence {
26-
pe_id String @id @default(uuid())
27-
pe_transaction_ref String
26+
pe_transaction_ref String @id
27+
pe_transaction_payload String @unique
2828
pe_transaction_date DateTime
2929
3030
pe_transaction_expect_amount Float @default(500.00)

src/common/guards/application-pass.guard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class ApplicationPassGuard implements CanActivate {
2929
const filterPassApplication = studentApplication.filter((app) => app.std_application_result === "pass");
3030

3131
if (filterPassApplication.length <= 0) {
32-
throw `Your Application in : ${studentApplication.map((app) => app.std_application_result).join(", ")} stage`;
32+
throw `Your Application not pass`;
3333
}
3434

3535
const filterAllowConfirm = filterPassApplication.filter((app) => app.stf_application_allow_confirm === true);

src/config/app.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,18 @@ export const config = {
5555
apis: {
5656
slipKey: process.env.API_EASYSLIP_KEY,
5757
},
58+
payment: {
59+
bypass: process.env.PAYMENT_BYPASS === "true",
60+
reciever: {
61+
name: {
62+
en: "MR. SIWACH G",
63+
th: "นาย ศิวัช ก",
64+
},
65+
account: {
66+
proxy: "004999224412568",
67+
real: "2178877804",
68+
},
69+
amount: 500,
70+
},
71+
},
5872
} as const;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ import { ApplicationConfirmationService } from "./application-confirmation.servi
55
@Module({
66
controllers: [ApplicationConfirmationController],
77
providers: [ApplicationConfirmationService],
8+
exports: [ApplicationConfirmationService],
89
})
910
export class ApplicationConfirmationModule {}

src/modules/application-payment-evidence/application-payment-evidence.controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export class ApplicationPaymentEvidenceController {
1111
constructor(private readonly applicationPaymentEvidenceService: ApplicationPaymentEvidenceService) {}
1212

1313
@Post("/upload")
14-
// @UseGuards(AnnounceAndConfirmPeriodGuard)
15-
// @UseGuards(ApplicationPassGuard)
14+
@UseGuards(AnnounceAndConfirmPeriodGuard)
15+
@UseGuards(ApplicationPassGuard)
1616
@UseInterceptors(FileInterceptor("file"))
1717
uploadEvidence(
1818
@Session() session: UserSession,

src/modules/application-payment-evidence/application-payment-evidence.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Module } from "@nestjs/common";
22
import { S3Module } from "src/core/s3/s3.module";
3+
import { ApplicationConfirmationModule } from "../application-confirmation/application-confirmation.module";
34
import { ApplicationPaymentEvidenceController } from "./application-payment-evidence.controller";
45
import { ApplicationPaymentEvidenceService } from "./application-payment-evidence.service";
56

67
@Module({
7-
imports: [S3Module],
8+
imports: [S3Module, ApplicationConfirmationModule],
89
controllers: [ApplicationPaymentEvidenceController],
910
providers: [ApplicationPaymentEvidenceService],
1011
})
Lines changed: 172 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import { PutObjectCommand } from "@aws-sdk/client-s3";
2-
import { HttpException, Injectable, InternalServerErrorException } from "@nestjs/common";
2+
import { HttpException, Injectable, InternalServerErrorException, NotAcceptableException } from "@nestjs/common";
33
import axios, { AxiosResponse } from "axios";
44
import { config } from "src/config/app.config";
55
import { LoggerService } from "src/core/logger/logger.service";
66
import { PrismaService } from "src/core/prisma/prisma.service";
77
import { S3Service } from "src/core/s3/s3.service";
88
import uuid from "uuid";
9+
import { ApplicationConfirmationService } from "../application-confirmation/application-confirmation.service";
910
import { MatchedAccount, RawSlip } from "./@types/VerifyBank.type";
1011
import { ApplicationPaymentEvidenceDto } from "./dto/application-payment-evidence.dto";
1112

12-
// Request
1313
interface VerifyByBase64Request {
14-
base64: string; // Base64 encoded image
15-
remark?: string; // 1-255 chars
14+
base64: string;
15+
remark?: string;
1616
matchAccount?: boolean;
1717
matchAmount?: number;
1818
checkDuplicate?: boolean;
1919
}
2020

21-
// Response
2221
interface VerifyBankResponse {
2322
success: true;
2423
data: VerifyBankData;
@@ -35,44 +34,154 @@ interface VerifyBankData {
3534
rawSlip: RawSlip;
3635
}
3736

38-
// See POST /verify/bank for full type definitions
39-
4037
@Injectable()
4138
export class ApplicationPaymentEvidenceService {
4239
constructor(
4340
private readonly prisma: PrismaService,
4441
private readonly logger: LoggerService,
4542
private readonly s3: S3Service,
43+
private readonly applicationConfirmationService: ApplicationConfirmationService,
4644
) {}
4745

4846
async uploadEvidence(userId: string, applicationPaymentEvidenceDto: ApplicationPaymentEvidenceDto, file: Express.Multer.File) {
4947
try {
50-
// const verifyBankResponse: AxiosResponse<VerifyBankResponse> = await axios.post<VerifyBankResponse, AxiosResponse<VerifyBankResponse>, VerifyByBase64Request>("https://api.easyslip.com/v2/verify/bank", {
51-
// base64: "file.buffer.toString('base64')"
52-
// }, {
53-
// headers: {
54-
// "Authorization": `Bearer ${config.apis.slipKey}`,
55-
// "Content-Type": "application/json"
56-
// }
57-
// });
58-
59-
// const response = await fetch('https://api.easyslip.com/v2/info', {
60-
// method: 'GET',
61-
// headers: {
62-
// 'Authorization': `Bearer ${config.apis.slipKey}`,
63-
// 'Content-Type': 'application/json'
64-
// },
65-
// body: JSON.stringify({ base64: file.buffer.toString("base64") })
66-
// });
67-
68-
const response = await fetch("https://api.easyslip.com/v2/info", {
69-
headers: {
70-
Authorization: `Bearer ${config.apis.slipKey}`,
48+
const base64WithMime = `data:${file.mimetype};base64,${file.buffer.toString("base64")}`;
49+
const verifyBankResponse: AxiosResponse<VerifyBankResponse> = await axios.post<VerifyBankResponse, AxiosResponse<VerifyBankResponse>, VerifyByBase64Request>(
50+
"https://api.easyslip.com/v2/verify/bank",
51+
{
52+
base64: base64WithMime,
53+
},
54+
{
55+
headers: {
56+
Authorization: `Bearer ${config.apis.slipKey}`,
57+
"Content-Type": "application/json",
58+
},
59+
},
60+
);
61+
62+
if (!verifyBankResponse.data.success) throw new InternalServerErrorException(verifyBankResponse.data.message);
63+
64+
if (!verifyBankResponse.data.data.rawSlip.receiver.account.name.en && !verifyBankResponse.data.data.rawSlip.receiver.account.name.th) {
65+
throw new NotAcceptableException(`Cannot find reciever name`);
66+
}
67+
68+
if (verifyBankResponse.data.data.rawSlip.receiver.account.name.en) {
69+
if (verifyBankResponse.data.data.rawSlip.receiver.account.name.en !== config.payment.reciever.name.en) {
70+
throw new NotAcceptableException(`Wrong reciever name en: ${verifyBankResponse.data.data.rawSlip.receiver.account.name.en}`);
71+
}
72+
}
73+
74+
if (verifyBankResponse.data.data.rawSlip.receiver.account.name.th) {
75+
if (verifyBankResponse.data.data.rawSlip.receiver.account.name.th !== config.payment.reciever.name.th && verifyBankResponse.data.data.rawSlip.receiver.account.name.th !== config.payment.reciever.name.en) {
76+
throw new NotAcceptableException(`Wrong reciever name th: ${verifyBankResponse.data.data.rawSlip.receiver.account.name.th}`);
77+
}
78+
}
79+
80+
if (!verifyBankResponse.data.data.rawSlip.receiver.account.bank?.account && !verifyBankResponse.data.data.rawSlip.receiver.account.proxy?.account) {
81+
throw new NotAcceptableException(`Cannot find account`);
82+
}
83+
84+
if (verifyBankResponse.data.data.rawSlip.receiver.account.bank?.account) {
85+
const regex = new RegExp(`^${verifyBankResponse.data.data.rawSlip.receiver.account.bank.account.toLowerCase().replace(/-/g, "").replace(/x/g, "\\d")}$`);
86+
if (!regex.test(config.payment.reciever.account.real)) {
87+
throw new NotAcceptableException(`Wrong Account: ${verifyBankResponse.data.data.rawSlip.receiver.account.bank?.account}`);
88+
}
89+
// if (verifyBankResponse.data.data.rawSlip.receiver.account.bank?.account !== config.payment.reciever.account_real) {}
90+
}
91+
92+
if (verifyBankResponse.data.data.rawSlip.receiver.account.proxy?.account) {
93+
const regex = new RegExp(`^${verifyBankResponse.data.data.rawSlip.receiver.account.proxy.account.toLowerCase().replace(/-/g, "").replace(/x/g, "\\d")}$`);
94+
if (!regex.test(config.payment.reciever.account.proxy)) {
95+
throw new NotAcceptableException(`Wrong Account: ${verifyBankResponse.data.data.rawSlip.receiver.account.proxy?.account}`);
96+
}
97+
// if (verifyBankResponse.data.data.rawSlip.receiver.account.proxy.account !== config.payment.reciever.account_proxy) {}
98+
}
99+
100+
// check isDuplicate
101+
const evidence = await this.prisma.applicationPaymentEvidence.count({
102+
where: {
103+
pe_transaction_ref: verifyBankResponse.data.data.rawSlip.transRef,
104+
},
105+
});
106+
107+
if (evidence !== 0) {
108+
throw new NotAcceptableException("This evidence has been used");
109+
}
110+
111+
const key = uuid.v4();
112+
113+
const createS3File = await this.s3
114+
.send(
115+
new PutObjectCommand({
116+
Bucket: config.s3.bucket,
117+
Key: key,
118+
Body: file.buffer,
119+
ContentType: file.mimetype,
120+
}),
121+
)
122+
.catch((e) => {
123+
throw e;
124+
});
125+
126+
const createFile = await this.prisma.applicationFile.create({
127+
data: {
128+
std_application_id: applicationPaymentEvidenceDto.application_id,
129+
std_file_key: key,
130+
std_file_type: "file_slip",
131+
std_file_originalname: file.originalname,
132+
std_file_mimetype: file.mimetype,
133+
std_file_encoding: file.encoding,
134+
std_file_size: file.size,
71135
},
72136
});
73137

74-
const result = await response.json();
75-
console.log(result.data);
138+
const createEvidence = await this.prisma.applicationPaymentEvidence.create({
139+
data: {
140+
pe_transaction_ref: verifyBankResponse.data.data.rawSlip.transRef,
141+
pe_transaction_payload: verifyBankResponse.data.data.rawSlip.payload,
142+
pe_transaction_date: new Date(verifyBankResponse.data.data.rawSlip.date),
143+
144+
pe_transaction_expect_amount: config.payment.reciever.amount,
145+
pe_transaction_actual_amount: verifyBankResponse.data.data.rawSlip.amount.amount,
146+
147+
pe_json: JSON.stringify(verifyBankResponse.data),
148+
149+
pe_sender_account_name: verifyBankResponse.data.data.rawSlip.sender.account.name.en || verifyBankResponse.data.data.rawSlip.sender.account.name.th || "",
150+
pe_sender_account_number: verifyBankResponse.data.data.rawSlip.sender.account.proxy?.account || "",
151+
152+
pe_sender_bank_id: verifyBankResponse.data.data.rawSlip.sender.bank?.id,
153+
pe_sender_bank_name: verifyBankResponse.data.data.rawSlip.sender.bank?.name,
154+
155+
pe_reciever_account_name: verifyBankResponse.data.data.rawSlip.receiver.account.name.en || verifyBankResponse.data.data.rawSlip.receiver.account.name.th || "",
156+
pe_reciever_account_number: verifyBankResponse.data.data.rawSlip.receiver.account.proxy?.account || "",
157+
158+
pe_reciever_bank_id: verifyBankResponse.data.data.rawSlip.receiver.bank?.id || "",
159+
pe_reciever_bank_name: verifyBankResponse.data.data.rawSlip.receiver.bank?.name || "",
160+
161+
std_file_key: createFile.std_file_key,
162+
std_application_id: createFile.std_application_id,
163+
},
164+
});
165+
166+
if ((await this.paymentStatusUpdater(userId, applicationPaymentEvidenceDto.application_id)) === false) {
167+
throw new NotAcceptableException(`Wrong amount: ${verifyBankResponse.data.data.rawSlip.amount.amount}`);
168+
}
169+
170+
await this.confirmUpdater(userId, applicationPaymentEvidenceDto.application_id);
171+
172+
const updatedEvidence = await this.prisma.applicationPaymentEvidence.findMany({
173+
where: {
174+
std_application_id: applicationPaymentEvidenceDto.application_id,
175+
std_application: {
176+
std_user_id: userId,
177+
},
178+
},
179+
include: {
180+
std_file: true,
181+
},
182+
});
183+
184+
return updatedEvidence;
76185
} catch (e) {
77186
console.log(e);
78187
this.logger.error(e);
@@ -84,54 +193,37 @@ export class ApplicationPaymentEvidenceService {
84193
}
85194
}
86195

87-
// async uploadEvidence(userId: string, applicationPaymentEvidenceDto: ApplicationPaymentEvidenceDto, file: Express.Multer.File) {
88-
// try {
89-
// // upload slip allow to access by url
90-
// const key = uuid.v4();
91-
// await this.s3.send(
92-
// new PutObjectCommand({
93-
// Bucket: config.s3.bucket,
94-
// Key: key,
95-
// Body: file.buffer,
96-
// ContentType: file.mimetype,
97-
// }),
98-
// );
99-
100-
// const slipVerificationResponse: AxiosResponse<SlipVerificationResponse> = await axios.post(
101-
// "https://connect.slip2go.com/api/verify-slip/qr-image-link/info",
102-
// {
103-
// payload: {
104-
// imageUrl: await this.s3.signedUrl(key),
105-
// },
106-
// },
107-
// {
108-
// headers: {
109-
// Authorization: `Bearer ${config.apis.slip2goKey}`,
110-
// },
111-
// },
112-
// );
113-
114-
// console.dir(slipVerificationResponse.data.data);
115-
116-
// // const newApplicationEvidenceFile = await this.prisma.applicationFile.create({
117-
// // data: {
118-
// // std_application_id: applicationPaymentEvidenceDto.application_id,
119-
// // std_file_key: key,
120-
// // std_file_type: "file_slip",
121-
// // std_file_originalname: file.originalname,
122-
// // std_file_mimetype: file.mimetype,
123-
// // std_file_encoding: file.encoding,
124-
// // std_file_size: file.size,
125-
// // },
126-
// // });
127-
// } catch (e) {
128-
// console.log(e);
129-
// this.logger.error(e);
130-
// if (e instanceof HttpException) {
131-
// throw e;
132-
// }
133-
134-
// throw new InternalServerErrorException(e);
135-
// }
136-
// }
196+
private async paymentStatusUpdater(userId: string, application_id: string) {
197+
const findEvidence = await this.prisma.applicationPaymentEvidence.findMany({
198+
where: {
199+
std_application_id: application_id,
200+
std_application: {
201+
std_user_id: userId,
202+
},
203+
},
204+
});
205+
206+
if (findEvidence.length === 0) return false;
207+
208+
const totalAmount = findEvidence.map((evd) => evd.pe_transaction_actual_amount).reduce((a: number, b: number) => a + b) ?? 0;
209+
console.log(totalAmount);
210+
if (totalAmount < config.payment.reciever.amount) return false;
211+
212+
await this.prisma.applicationStatus.update({
213+
where: {
214+
std_application_id: application_id,
215+
std_application: {
216+
std_user_id: userId,
217+
},
218+
},
219+
data: {
220+
std_status_payment_done: true,
221+
},
222+
});
223+
return true;
224+
}
225+
226+
private async confirmUpdater(userId: string, application_id: string) {
227+
await this.applicationConfirmationService.isConfirmApplication(userId, { application_id: application_id, confirm: true, reason: "" });
228+
}
137229
}

src/modules/application-status/application-status.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export class ApplicationStatusService {
1818
std_user_id: userId,
1919
},
2020
},
21+
include: {
22+
std_application: true,
23+
},
2124
});
2225

2326
return applicationStatus ? applicationStatus : new NotFoundException();

0 commit comments

Comments
 (0)