Skip to content
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
23 changes: 0 additions & 23 deletions src/settlements/constants/bank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,28 +62,5 @@ export const PAYPLE_BANKS: Record<string, string> = {
"304": "ํ˜„๋Œ€์บํ”ผํƒˆ",
};

// Payple ๋ช…์„ธ์—์„œ ์ œ๊ฑฐ๋œ ์ฝ”๋“œ โ€” ๊ธฐ์กด ๋“ฑ๋ก ์‚ฌ์šฉ์ž ํ˜ธํ™˜ ๋ณด์กด์šฉ.
// ์‹ ๊ทœ ๋“ฑ๋ก/์ธ์ฆ์—๋Š” ์‚ฌ์šฉ ๊ธˆ์ง€. Phase 11 ์šด์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„ ์ œ๊ฑฐ ์˜ˆ์ •.
export const LEGACY_PAYPLE_BANKS: Record<string, string> = {
"008": "์ˆ˜์ถœ์ž…์€ํ–‰",
"054": "HSBC์€ํ–‰",
"055": "๋„์ด์น˜์€ํ–‰",
"057": "์ œ์ดํ”ผ๋ชจ๊ฐ„์ฒด์ด์Šค์€ํ–‰",
"058": "๋ฏธ์ฆˆํ˜ธ์€ํ–‰",
"059": "์— ์œ ์—ํ”„์ง€์€ํ–‰",
"060": "BOA์€ํ–‰",
"061": "๋น„์—”ํ”ผํŒŒ๋ฆฌ๋ฐ”์€ํ–‰",
"062": "์ค‘๊ตญ๊ณต์ƒ์€ํ–‰",
"063": "์ค‘๊ตญ์€ํ–‰",
"067": "์ค‘๊ตญ๊ฑด์„ค์€ํ–‰",
"076": "์‹ ์šฉ๋ณด์ฆ๊ธฐ๊ธˆ",
"077": "๊ธฐ์ˆ ๋ณด์ฆ๊ธฐ๊ธˆ",
};

export const ALL_KNOWN_BANKS: Record<string, string> = { ...PAYPLE_BANKS, ...LEGACY_PAYPLE_BANKS };

export const isValidPaypleBank = (code: string): boolean =>
Object.prototype.hasOwnProperty.call(PAYPLE_BANKS, code);

export const isKnownBank = (code: string): boolean =>
Object.prototype.hasOwnProperty.call(ALL_KNOWN_BANKS, code);
33 changes: 32 additions & 1 deletion src/settlements/controllers/settlement.account.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Request, Response } from 'express';
import { verifySellerAccount, getAccountInfo } from '../services/settlement.account.service';
import {
verifySellerAccount,
getAccountInfo,
getSellerAccountDetail,
} from '../services/settlement.account.service';
import { VerifyAccountRequestDto, ViewAccountResponseDto } from '../dtos/settlement.dto';

export const verifyAccount = async (req: Request, res: Response) => {
Expand Down Expand Up @@ -56,6 +60,33 @@ export const verifyAccount = async (req: Request, res: Response) => {
}
};

export const ViewAccountDetail = async (req: Request, res: Response) => {
try {
const user = req.user;
if (!user) {
return res.status(401).json({
error: 'Unauthorized',
message: '๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.',
statusCode: 401,
});
}
const userId = (req.user as { user_id: number }).user_id;
const data = await getSellerAccountDetail(userId);
return res.status(200).json({
message: 'ํŒ๋งค์ž ์ •๋ณด ์กฐํšŒ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
data,
statusCode: 200,
});
} catch (error: any) {
const status = error.statusCode || 500;
return res.status(status).json({
error: error.error || 'InternalServerError',
message: error.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.',
statusCode: status,
});
}
};

export const ViewAccount = async (req: Request, res: Response) => {
try {
const user = req.user;
Expand Down
4 changes: 4 additions & 0 deletions src/settlements/controllers/settlement.seller.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export const registerIndividual = async (req: Request, res: Response) => {

return res.status(200).json({
message: result.message,
status: result.status,
requiresApproval: result.requiresApproval,
statusCode: 200,
});
} catch (error: any) {
Expand Down Expand Up @@ -157,6 +159,8 @@ export const registerBusiness = async (req: Request, res: Response) => {

return res.status(200).json({
message: result.message,
status: result.status,
requiresApproval: result.requiresApproval,
statusCode: 200,
});
} catch (error: any) {
Expand Down
26 changes: 25 additions & 1 deletion src/settlements/dtos/settlement.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export interface RegisterIndividualSellerRequestDto {
export interface RegisterBusinessSellerRequestDto {
registerToken: string;
companyName: string;
businessLicenseUrl: string; // S3 ์—…๋กœ๋“œ ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ URL (Phase 9์—์„œ key๋กœ ์ „ํ™˜ ์˜ˆ์ •)
// ์ •๋ณด ๋ณ€๊ฒฝ ์‹œ ์ƒˆ ํŒŒ์ผ ์—…๋กœ๋“œ ์•ˆ ํ•˜๊ณ  ๊ธฐ์กด ๋“ฑ๋ก์ฆ ์œ ์ง€ํ•˜๊ณ  ์‹ถ์œผ๋ฉด ์ƒ๋žต ๊ฐ€๋Šฅ
businessLicenseUrl?: string;
isTermsAgreed: boolean;
}

Expand All @@ -51,3 +52,26 @@ export interface UpdateAccountRequestDto {
accountNumber: string;
holderName: string;
}

// ์ •๋ณด ๋ณ€๊ฒฝ ํ™”๋ฉด prefill์šฉ โ€” ์‚ฌ์—…์ž๋Š” ์ถ”๊ฐ€ ํ•„๋“œ ํฌํ•จ, businessNumber๋Š” ๋งˆ์Šคํ‚น
export interface AccountDetailResponseDto {
sellerType: SellerKind;
status: 'APPROVED' | 'PENDING' | 'REJECTED';
isActive: boolean;
bank: string;
accountNumber: string;
holderName: string;
name: string;
birthDate?: string; // YYMMDD. ๋ณธ์ธ ์กฐํšŒ์šฉ ํ‰๋ฌธ. INDIVIDUAL / BUSINESS+PERSONAL์ผ ๋•Œ ๊ฐ’ ์กด์žฌ
businessType?: BusinessKind;
businessNumber?: string; // ๋งˆ์Šคํ‚น๋œ ํ‘œ์‹œ๊ฐ’
representativeName?: string;
companyName?: string;
businessLicenseUrl?: string;
}

export interface RegisterResultDto {
message: string;
status: 'APPROVED' | 'PENDING';
requiresApproval: boolean;
}
48 changes: 47 additions & 1 deletion src/settlements/repositories/settlement.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface UpsertIndividualAccountInput {
bank: string;
accountNumber: string;
holderName: string;
birthDate: string; // INDIVIDUAL์€ Payple ์ธ์ฆ์— birthDate ํ•„์ˆ˜
}

export interface CreateBusinessAccountInput {
Expand All @@ -17,6 +18,20 @@ export interface CreateBusinessAccountInput {
businessType: BusinessKind;
companyName: string;
businessLicenseUrl: string;
birthDate?: string; // BUSINESS+PERSONAL์ผ ๋•Œ๋งŒ ์กด์žฌ (๋Œ€ํ‘œ์ž ์ƒ๋…„์›”์ผ). CORPORATE๋Š” undefined
}

export interface UpdateBusinessAccountInput {
representativeName: string;
bank: string;
accountNumber: string;
holderName: string;
businessNumber: string;
businessType: BusinessKind;
companyName: string;
// optional โ€” ๋นˆ ๊ฐ’์ด๋ฉด ๊ธฐ์กด URL ์œ ์ง€
businessLicenseUrl?: string | null;
birthDate?: string; // BUSINESS+PERSONAL์ผ ๋•Œ๋งŒ ์กด์žฌ
}

export const SettlementRepository = {
Expand All @@ -30,11 +45,11 @@ export const SettlementRepository = {
bank_code: dto.bank,
account_number: dto.accountNumber,
account_holder: dto.holderName,
birth_date: dto.birthDate,
seller_type: 'INDIVIDUAL',
business_type: null,
status: 'APPROVED',
is_active: true,
birth_date: null,
business_number: null,
company_name: null,
representative_name: null,
Expand All @@ -45,6 +60,7 @@ export const SettlementRepository = {
bank_code: dto.bank,
account_number: dto.accountNumber,
account_holder: dto.holderName,
birth_date: dto.birthDate,
seller_type: 'INDIVIDUAL',
status: 'APPROVED',
is_active: true,
Expand Down Expand Up @@ -76,13 +92,43 @@ export const SettlementRepository = {
company_name: dto.companyName,
representative_name: dto.representativeName,
business_license_url: dto.businessLicenseUrl,
birth_date: dto.birthDate ?? null,
seller_type: 'BUSINESS',
status: 'PENDING',
is_active: false,
},
});
},

// ์‚ฌ์—…์ž โ†’ ์‚ฌ์—…์ž ์ •๋ณด๋ณ€๊ฒฝ.
// ๊ฐ™์€ row ๋ฎ์–ด์“ฐ๊ธฐ + status=PENDING + is_active=false (๊ด€๋ฆฌ์ž ์Šน์ธ ์ „๊นŒ์ง€ ์ผ์‹œ ๋น„ํ™œ์„ฑํ™”).
// businessLicenseUrl์ด undefined๋ฉด ๊ธฐ์กด URL ์œ ์ง€.
updateBusinessAccountForApproval: async (
userId: number,
dto: UpdateBusinessAccountInput,
) => {
const data: Record<string, unknown> = {
bank_code: dto.bank,
account_number: dto.accountNumber,
account_holder: dto.holderName,
business_number: dto.businessNumber,
business_type: dto.businessType,
company_name: dto.companyName,
representative_name: dto.representativeName,
birth_date: dto.birthDate ?? null,
seller_type: 'BUSINESS',
status: 'PENDING',
is_active: false,
};
if (dto.businessLicenseUrl) {
data.business_license_url = dto.businessLicenseUrl;
}
return await prisma.settlementAccount.update({
where: { user_id: userId },
data,
});
},

deleteAccountByUserId: async (userId: number) => {
return await prisma.settlementAccount.delete({
where: { user_id: userId },
Expand Down
73 changes: 66 additions & 7 deletions src/settlements/routes/settlement.route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Router } from "express";
import { verifyAccount, ViewAccount } from "../controllers/settlement.account.controller";
import { verifyAccount, ViewAccount, ViewAccountDetail } from "../controllers/settlement.account.controller";
import { registerIndividual, registerBusiness } from "../controllers/settlement.seller.controller";
import { uploadLicense } from "../controllers/settlement.seller.controller";
import { getMonthlySales, getYearlySettlements } from "../controllers/settlement.history.controller";
Expand Down Expand Up @@ -199,6 +199,54 @@ router.post("/verify-account", authenticateJwt, verifyAccount);
*/
router.get("/accounts", authenticateJwt, ViewAccount);

/**
* @swagger
* /api/settlements/account/detail:
* get:
* summary: ํŒ๋งค์ž ์ •๋ณด ๋ณ€๊ฒฝ ํ™”๋ฉด์šฉ ์ƒ์„ธ ์กฐํšŒ
* description: |
* ์ •๋ณด ๋ณ€๊ฒฝ ํ™”๋ฉด์—์„œ ๊ธฐ์กด ๋“ฑ๋ก ๋ฐ์ดํ„ฐ๋ฅผ prefill ํ•˜๊ธฐ ์œ„ํ•œ ์ƒ์„ธ ์กฐํšŒ API.
* ์‚ฌ์—…์ž๋Š” ์ถ”๊ฐ€ ํ•„๋“œ(businessType/businessNumber/companyName/representativeName/businessLicenseUrl/status) ํฌํ•จ.
* businessNumber๋Š” ๋งˆ์Šคํ‚น(`123-45-****0`)๋˜์–ด ์‘๋‹ต๋˜๋ฉฐ, ๋ณ€๊ฒฝ ์‹œ ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ œ ๊ฐ’์„ ๋‹ค์‹œ ์ž…๋ ฅํ•ด์•ผ ํ•จ.
* birthDate๋Š” ๋ณธ์ธ ์กฐํšŒ์šฉ ํ‰๋ฌธ์œผ๋กœ ์‘๋‹ต (์ •๋ณด ๋ณ€๊ฒฝ ์‹œ ๊ณ„์ขŒ ์žฌ์ธ์ฆ์„ ์œ„ํ•ด prefill ํ•„์š”).
* tags: [Settlement]
* security:
* - jwt: []
* responses:
* 200:
* description: ์กฐํšŒ ์„ฑ๊ณต
* content:
* application/json:
* schema:
* type: object
* properties:
* message: { type: string, example: ํŒ๋งค์ž ์ •๋ณด ์กฐํšŒ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. }
* data:
* type: object
* properties:
* sellerType: { type: string, enum: [INDIVIDUAL, BUSINESS] }
* status: { type: string, enum: [APPROVED, PENDING, REJECTED] }
* isActive: { type: boolean }
* bank: { type: string, example: "088" }
* accountNumber: { type: string, example: "1234567890" }
* holderName: { type: string, example: ํ™๊ธธ๋™ }
* name: { type: string, description: ์‹ค๋ช…(INDIVIDUAL) ๋˜๋Š” ๋Œ€ํ‘œ์ž๋ช…(BUSINESS) }
* birthDate: { type: string, nullable: true, description: ์˜ˆ๊ธˆ์ฃผ ์ƒ๋…„์›”์ผ YYMMDD. CORPORATE๋Š” null, example: "880212" }
* businessType: { type: string, enum: [PERSONAL, CORPORATE], nullable: true }
* businessNumber: { type: string, nullable: true, description: ๋งˆ์Šคํ‚น๋œ ์‚ฌ์—…์ž๋ฒˆํ˜ธ, example: "123-45-****0" }
* representativeName: { type: string, nullable: true }
* companyName: { type: string, nullable: true }
* businessLicenseUrl: { type: string, nullable: true }
* statusCode: { type: integer, example: 200 }
* 401:
* description: ๋กœ๊ทธ์ธ ํ•„์š”
* 404:
* description: ๋“ฑ๋ก๋œ ํŒ๋งค์ž ์ •๋ณด ์—†์Œ
* 500:
* description: ์„œ๋ฒ„ ์˜ค๋ฅ˜
*/
router.get("/account/detail", authenticateJwt, ViewAccountDetail);

/**
* @swagger
* /api/settlements/register/individual:
Expand Down Expand Up @@ -237,6 +285,8 @@ router.get("/accounts", authenticateJwt, ViewAccount);
* type: object
* properties:
* message: { type: string, example: ๊ฐœ์ธ ํŒ๋งค์ž ๋“ฑ๋ก์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. }
* status: { type: string, enum: [APPROVED], example: APPROVED }
* requiresApproval: { type: boolean, example: false, description: ๊ฐœ์ธ ํŒ๋งค์ž๋Š” ์ฆ‰์‹œ ์Šน์ธ์ด๋ฏ€๋กœ ํ•ญ์ƒ false }
* statusCode: { type: integer, example: 200 }
* 400:
* description: ํ•„์ˆ˜๊ฐ’ ๋ˆ„๋ฝ ๋˜๋Š” ์•ฝ๊ด€ ๋ฏธ๋™์˜
Expand Down Expand Up @@ -318,10 +368,18 @@ router.post("/upload/business-license", authenticateJwt, uploadLicense);
* @swagger
* /api/settlements/register/business:
* post:
* summary: ์‚ฌ์—…์ž ํŒ๋งค์ž ๋“ฑ๋ก ์‹ ์ฒญ
* summary: ์‚ฌ์—…์ž ํŒ๋งค์ž ๋“ฑ๋ก/๋ณ€๊ฒฝ ์‹ ์ฒญ
* description: |
* /verify-account์—์„œ ๋ฐœ๊ธ‰๋ฐ›์€ registerToken๊ณผ ์ถ”๊ฐ€ ์‚ฌ์—…์ž ์ •๋ณด(์ƒํ˜ธ๋ช…, ์‚ฌ์—…์ž๋“ฑ๋ก์ฆ)๋ฅผ ๋ฐ›์•„
* ์‚ฌ์—…์ž ํŒ๋งค์ž๋กœ ๋“ฑ๋ก ์‹ ์ฒญํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ๋Š” PENDING์œผ๋กœ ์ €์žฅ๋˜๊ณ  ๊ด€๋ฆฌ์ž ์Šน์ธ ํ›„ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.
* ์‚ฌ์—…์ž ํŒ๋งค์ž๋กœ ๋“ฑ๋ก ๋˜๋Š” ๋ณ€๊ฒฝ ์‹ ์ฒญํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ์ƒํƒœ๋Š” PENDING์œผ๋กœ ์ €์žฅ๋˜๊ณ  ๊ด€๋ฆฌ์ž ์Šน์ธ ํ›„ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.
*
* ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ๋™์ž‘:
* - ์ตœ์ดˆ ์‚ฌ์—…์ž ๋“ฑ๋ก: ์‹ ๊ทœ row ์ƒ์„ฑ (PENDING, is_active=false)
* - ๊ฐœ์ธ โ†’ ์‚ฌ์—…์ž ์ „ํ™˜: ๊ธฐ์กด INDIVIDUAL row ์‚ญ์ œ + ์‹ ๊ทœ BUSINESS row ์ƒ์„ฑ (PENDING)
* - ์‚ฌ์—…์ž โ†’ ์‚ฌ์—…์ž ์ •๋ณด ๋ณ€๊ฒฝ: ๊ฐ™์€ row ๋ฎ์–ด์“ฐ๊ธฐ + status=PENDING + is_active=false (์Šน์ธ ์ „๊นŒ์ง€ ์ผ์‹œ ๋น„ํ™œ์„ฑํ™”)
*
* ์‚ฌ์—…์ž๋“ฑ๋ก์ฆ(businessLicenseUrl)์€ ์‚ฌ์—…์ž โ†’ ์‚ฌ์—…์ž ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ์ƒ๋žต ๊ฐ€๋Šฅ (๊ธฐ์กด URL ์œ ์ง€).
* ์ตœ์ดˆ ๋“ฑ๋ก / ๊ฐœ์ธโ†’์‚ฌ์—…์ž ์ „ํ™˜์—๋Š” ํ•„์ˆ˜.
* tags: [Settlement]
* security:
* - jwt: []
Expand All @@ -334,7 +392,6 @@ router.post("/upload/business-license", authenticateJwt, uploadLicense);
* required:
* - registerToken
* - companyName
* - businessLicenseUrl
* - isTermsAgreed
* properties:
* registerToken:
Expand All @@ -345,7 +402,7 @@ router.post("/upload/business-license", authenticateJwt, uploadLicense);
* example: (์ฃผ)ํ”„๋กฌํ”„ํŠธํŒฉํ† ๋ฆฌ
* businessLicenseUrl:
* type: string
* description: /upload/business-license ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ fileUrl ๋˜๋Š” fileKey
* description: /upload/business-license ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ fileUrl. ์‚ฌ์—…์žโ†’์‚ฌ์—…์ž ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ์ƒ๋žต ๊ฐ€๋Šฅ
* isTermsAgreed:
* type: boolean
* example: true
Expand All @@ -357,7 +414,9 @@ router.post("/upload/business-license", authenticateJwt, uploadLicense);
* schema:
* type: object
* properties:
* message: { type: string, example: ์‚ฌ์—…์ž ํŒ๋งค์ž ์‹ ์ฒญ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž ์Šน์ธ ํ›„ ์ตœ์ข… ๋“ฑ๋ก๋ฉ๋‹ˆ๋‹ค. }
* message: { type: string, description: ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€, example: ์‚ฌ์—…์ž ํŒ๋งค์ž ์‹ ์ฒญ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž ์Šน์ธ ํ›„ ์ตœ์ข… ๋“ฑ๋ก๋ฉ๋‹ˆ๋‹ค. }
* status: { type: string, enum: [PENDING], example: PENDING }
* requiresApproval: { type: boolean, example: true, description: ์‚ฌ์—…์ž๋Š” ๊ด€๋ฆฌ์ž ์Šน์ธ ํ•„์š” โ€” ํ•ญ์ƒ true }
* statusCode: { type: integer, example: 200 }
* 400:
* description: ํ•„์ˆ˜๊ฐ’ ๋ˆ„๋ฝ ๋˜๋Š” ์•ฝ๊ด€ ๋ฏธ๋™์˜
Expand All @@ -370,7 +429,7 @@ router.post("/upload/business-license", authenticateJwt, uploadLicense);
* schema:
* type: object
* properties:
* error: { type: string, description: "RegisterTokenAlreadyUsed | AlreadyRegistered | DuplicateBusinessNumber" }
* error: { type: string, description: "RegisterTokenAlreadyUsed | DuplicateBusinessNumber" }
* message: { type: string }
* statusCode: { type: integer, example: 409 }
* 500:
Expand Down
Loading
Loading