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
30 changes: 29 additions & 1 deletion app/src/features/funding/candidates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const BASE_PRODUCT: Record<string, unknown> = {
region: { scope: 'SEOUL', districtCodes: [], note: null },
purpose: { included: ['OPERATING_FUNDS'], excluded: [], note: null },
industryConditions: { scope: 'UNRESTRICTED', included: [], excluded: [], note: null },
applicationPeriod: { start: '2026-09-01', end: '2026-12-31', note: null },
applicationPeriod: { start: '2026-09-01', end: FAR_FUTURE, note: null },
observedApplicationStatus: 'OPEN',
observedAt: TEST_DATE,
reviewedAt: TEST_DATE,
Expand Down Expand Up @@ -311,6 +311,34 @@ describe('후보 판정과 응답', () => {
expect(payload.summary.CURRENT_CANDIDATE).toBe(0);
});

it('신청 종료일이 지나면 접수 중으로 기록된 상품도 접수 종료로 분리한다', async () => {
const { prisma } = stubPrisma({
products: [
membership(rawProduct({ applicationPeriod: { start: '2026-09-01', end: '2026-09-30', note: null } }), 0),
],
});
const lastDay = await candidatesOf(prisma, PRE_PROFILE, '2026-09-30');
expect(lastDay.evaluations[0].candidateStatus).toBe('CURRENT_CANDIDATE');

const after = await candidatesOf(prisma, PRE_PROFILE, '2026-10-01');
const [evaluation] = after.evaluations;
expect(evaluation.observedApplicationStatus).toBe('OPEN');
expect(evaluation.candidateStatus).toBe('CLOSED');
expect(evaluation.candidateReason).toContain('2026-09-30');
expect(after.summary.CLOSED).toBe(1);
expect(after.summary.CURRENT_CANDIDATE).toBe(0);
});

it('신청 종료일이 없으면 관측 접수 상태를 따른다', async () => {
const { prisma } = stubPrisma({
products: [
membership(rawProduct({ applicationPeriod: { start: '2026-09-01', end: null, note: '예산 소진 시까지' } }), 0),
],
});
const payload = await candidatesOf(prisma, PRE_PROFILE, '2026-12-31');
expect(payload.evaluations[0].candidateStatus).toBe('CURRENT_CANDIDATE');
});

it('검수 기한이 지난 상품은 접수 중이어도 검수 기한 경과로 분리한다', async () => {
const { prisma } = stubPrisma({
products: [
Expand Down
7 changes: 7 additions & 0 deletions app/src/features/funding/eligibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { FundingCatalog, FundingProduct } from './schema.ts';
import {
REPAYABLE_SUPPORT_TYPES,
combineVerdicts,
compareIsoDates,
INDUSTRY_CODE_PATTERN,
isPrimaryRetrievalMethod,
isSupportedRepaymentMethod,
Expand Down Expand Up @@ -342,6 +343,9 @@ export function evaluateProduct(
const state = reviewState(product, options.asOfDate);
const repayment = assessProductRepayment(product);
const primaryEvidenceVerified = hasPrimaryEvidence(product);
// 관측 접수 상태가 OPEN·UNKNOWN이어도 신청 종료일이 지나면 접수가 끝난 것이다. 종료일 당일까지는 접수 중으로 본다.
const periodEnd = product.applicationPeriod.end;
const periodEnded = periodEnd !== null && compareIsoDates(periodEnd, options.asOfDate) < 0;

let candidateStatus: FundingCandidateStatus;
let candidateReason: string;
Expand All @@ -351,6 +355,9 @@ export function evaluateProduct(
} else if (product.observedApplicationStatus === 'CLOSED') {
candidateStatus = 'CLOSED';
candidateReason = `${product.observedAt} 기준 접수 상태가 CLOSED입니다.`;
} else if (periodEnded) {
candidateStatus = 'CLOSED';
candidateReason = `신청 종료일 ${periodEnd}이(가) 기준일 ${options.asOfDate} 이전이라 접수가 끝난 것으로 판정합니다.`;
} else if (state === 'UNREVIEWED') {
candidateStatus = 'NEEDS_CONFIRMATION';
candidateReason =
Expand Down
2 changes: 1 addition & 1 deletion app/src/features/plans/funding-matches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const PRODUCT: Record<string, unknown> = {
region: { scope: 'SEOUL', districtCodes: [], note: null },
purpose: { included: ['OPERATING_FUNDS'], excluded: [], note: null },
industryConditions: { scope: 'UNRESTRICTED', included: [], excluded: [], note: null },
applicationPeriod: { start: '2026-09-01', end: '2026-12-31', note: null },
applicationPeriod: { start: '2026-09-01', end: FAR_FUTURE, note: null },
observedApplicationStatus: 'OPEN',
observedAt: TEST_DATE,
reviewedAt: TEST_DATE,
Expand Down
2 changes: 1 addition & 1 deletion app/src/features/plans/loan-assumption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const BASE_PRODUCT: Record<string, unknown> = {
region: { scope: 'SEOUL', districtCodes: [], note: null },
purpose: { included: ['OPERATING_FUNDS'], excluded: [], note: null },
industryConditions: { scope: 'UNRESTRICTED', included: [], excluded: [], note: null },
applicationPeriod: { start: '2026-09-01', end: '2026-12-31', note: null },
applicationPeriod: { start: '2026-09-01', end: FAR_FUTURE, note: null },
observedApplicationStatus: 'OPEN',
observedAt: TEST_DATE,
reviewedAt: TEST_DATE,
Expand Down
1 change: 1 addition & 0 deletions docs/domains/funding.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- 판정 규칙(`eligibility.ts`): 조건별로 PASS/FAIL/UNKNOWN을 계산한다. FAIL이 하나라도 있으면 FAIL, FAIL 없이 UNKNOWN이 있으면 UNKNOWN, 모두 PASS면 PASS다.
- REGION·PURPOSE·INDUSTRY를 포함한 각 조건에 직접 확인한 공식 근거가 없으면 UNKNOWN이다.
- 접수 상태 OPEN/CLOSED/UNKNOWN은 자격 판정과 별도로 유지한다.
- 신청 종료일이 판정 기준일보다 앞서면 관측 접수 상태가 OPEN·UNKNOWN이어도 접수 종료(CLOSED)로 판정한다. 종료일 당일까지는 접수 중으로 보고, 종료일이 없으면(예: 예산 소진 시까지) 관측 접수 상태를 따른다.
- 검수 기한이 지난 상품, 검수일 또는 검수자가 없는 상품, 검색 결과 요약으로만 확인한 상품은 현재 후보로 확정하지 않는다.
- 사업자등록 이후에만 검토할 수 있는 상품은 POST_REGISTRATION 상태로 현재 예비 창업자 후보와 분리한다.
- 지원 유형 표시: GRANT 지원금, GUARANTEE 보증, LOAN 대출, SPACE 공간·보육, PROGRAM 프로그램.
Expand Down
Loading