diff --git a/package.json b/package.json index d19dff297..902ff6c82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.1+9", + "version": "0.8.1+10", "private": false, "scripts": { "dev": "next dev", diff --git a/src/constants/zod_schema.ts b/src/constants/zod_schema.ts index 0ed66dea6..8fe786fe9 100644 --- a/src/constants/zod_schema.ts +++ b/src/constants/zod_schema.ts @@ -9,6 +9,7 @@ import { journalGetByIdValidator, journalListValidator, } from '@/lib/utils/zod_schema/journal'; +import { kycUploadValidator } from '@/lib/utils/zod_schema/kyc'; import { ocrDeleteValidator, ocrListValidator, @@ -43,4 +44,5 @@ export const API_ZOD_SCHEMA = { [APIName.JOURNAL_LIST]: journalListValidator, [APIName.JOURNAL_GET_BY_ID]: journalGetByIdValidator, [APIName.JOURNAL_DELETE]: journalDeleteValidator, + [APIName.KYC_UPLOAD]: kycUploadValidator, }; diff --git a/src/interfaces/company_kyc.ts b/src/interfaces/company_kyc.ts index 7ccbbe76c..613c3b3ff 100644 --- a/src/interfaces/company_kyc.ts +++ b/src/interfaces/company_kyc.ts @@ -48,7 +48,7 @@ export interface ICompanyKYCForm { [RegistrationInfoKeys.COUNTRY]: CountryOptions; [RegistrationInfoKeys.LEGAL_STRUCTURE]: LegalStructureOptions; [RegistrationInfoKeys.BUSINESS_REGISTRATION_NUMBER]: string; - [RegistrationInfoKeys.REGISTRATION_DATE]: string; + [RegistrationInfoKeys.REGISTRATION_DATE]: string; // Info: (20240912 - Murky) Maybe this suppose to be number? [RegistrationInfoKeys.INDUSTRY]: IndustryOptions; [ContactInfoKeys.KEY_CONTACT_PERSON]: string; [ContactInfoKeys.CONTACT_PHONE]: string; diff --git a/src/lib/utils/repo/company_kyc.repo.ts b/src/lib/utils/repo/company_kyc.repo.ts index 60a733803..6c88cd411 100644 --- a/src/lib/utils/repo/company_kyc.repo.ts +++ b/src/lib/utils/repo/company_kyc.repo.ts @@ -15,7 +15,22 @@ export async function createCompanyKYC( const companyKYC: CompanyKYC = await prisma.companyKYC.create({ data: { companyId, - ...companyKYCForm, + // ...companyKYCForm, + legalName: companyKYCForm.legalName, + city: companyKYCForm.city, + zipCode: companyKYCForm.zipCode, + address: companyKYCForm.address, + representativeName: companyKYCForm.representativeName, + country: companyKYCForm.country, + structure: companyKYCForm.structure, + registrationNumber: companyKYCForm.registrationNumber, + registrationDate: companyKYCForm.registrationDate, + industry: companyKYCForm.industry, + contactPerson: companyKYCForm.contactPerson, + contactPhone: companyKYCForm.contactPhone, + contactEmail: companyKYCForm.contactEmail, + website: companyKYCForm.website, + representativeIdType: companyKYCForm.representativeIdType, registrationCertificateFileId: companyKYCForm.registrationCertificateFileId, taxCertificateFileId: companyKYCForm.taxCertificateFileId, representativeIdCardFileId: companyKYCForm.representativeIdCardFileId, diff --git a/src/lib/utils/report/report_401_generator.ts b/src/lib/utils/report/report_401_generator.ts index 16fb7549c..b6fbf54ca 100644 --- a/src/lib/utils/report/report_401_generator.ts +++ b/src/lib/utils/report/report_401_generator.ts @@ -12,11 +12,11 @@ import { } from '@/interfaces/report'; import { getCompanyKYCByCompanyId } from '@/lib/utils/repo/company_kyc.repo'; import { convertTimestampToROCDate } from '@/lib/utils/common'; -import { STATUS_MESSAGE } from '@/constants/status_code'; import { listJournalFor401 } from '@/lib/utils/repo/journal.repo'; import { SPECIAL_ACCOUNTS } from '@/constants/account'; import { IJournalIncludeVoucherLineItemsInvoicePayment } from '@/interfaces/journal'; import { importsCategories, purchasesCategories, salesCategories } from '@/constants/invoice'; +import { CompanyKYC } from '@prisma/client'; export default class Report401Generator extends ReportGenerator { constructor(companyId: number, startDateInSecond: number, endDateInSecond: number) { @@ -198,20 +198,21 @@ export default class Report401Generator extends ReportGenerator { from: number, to: number ): Promise { - const companyKYC = await getCompanyKYCByCompanyId(companyId); + const companyKYC: CompanyKYC | null = await getCompanyKYCByCompanyId(companyId); if (!companyKYC) { - throw new Error(STATUS_MESSAGE.FORBIDDEN); + // Info: (20240912 - Murky) temporary allow to generate report without KYC + // throw new Error(STATUS_MESSAGE.FORBIDDEN); } const ROCStartDate = convertTimestampToROCDate(from); const ROCEndDate = convertTimestampToROCDate(to); // 1. 獲取所有發票 const journalList = await listJournalFor401(companyId, from, to); const basicInfo = { - uniformNumber: companyKYC.registrationNumber, - businessName: companyKYC.legalName, - personInCharge: companyKYC.representativeName, + uniformNumber: companyKYC?.registrationNumber ?? '', + businessName: companyKYC?.legalName ?? '', + personInCharge: companyKYC?.representativeName ?? '', taxSerialNumber: 'ABC123', // TODO (20240808 - Jacky): Implement this field in next sprint - businessAddress: companyKYC.address, + businessAddress: companyKYC?.address ?? '', currentYear: ROCStartDate.year.toString(), startMonth: ROCStartDate.month.toString(), endMonth: ROCEndDate.month.toString(), diff --git a/src/lib/utils/type_guard/company_kyc.ts b/src/lib/utils/type_guard/company_kyc.ts index c071e6876..bd7ab0000 100644 --- a/src/lib/utils/type_guard/company_kyc.ts +++ b/src/lib/utils/type_guard/company_kyc.ts @@ -1,80 +1,83 @@ import { - BasicInfoKeys, - ContactInfoKeys, CountryOptions, IndustryOptions, LegalStructureOptions, - RegistrationInfoKeys, RepresentativeIDType, - UploadDocumentKeys, } from '@/constants/kyc'; import { ICompanyKYC, ICompanyKYCForm } from '@/interfaces/company_kyc'; -import { getEnumValue } from '@/lib/utils/common'; -import { CompanyKYC } from '@prisma/client'; +import { iCompanyKYCFormValidator, iCompanyKYCValidator } from '@/lib/utils/zod_schema/kyc'; -export function isCompanyKYC(data: CompanyKYC): data is ICompanyKYC { - return ( - typeof data.id === 'number' && - typeof data.companyId === 'number' && - typeof data.legalName === 'string' && - Object.values(CountryOptions).includes(data.country as CountryOptions) && - typeof data.city === 'string' && - typeof data.address === 'string' && - typeof data.zipCode === 'string' && - typeof data.representativeName === 'string' && - Object.values(LegalStructureOptions).includes(data.structure as LegalStructureOptions) && - typeof data.registrationNumber === 'string' && - typeof data.registrationDate === 'string' && - Object.values(IndustryOptions).includes(data.industry as IndustryOptions) && - typeof data.contactPerson === 'string' && - typeof data.contactPhone === 'string' && - typeof data.contactEmail === 'string' && - // (typeof data.website === 'string' || data.website === undefined) && Info: (20240719 - Tzuhan) this field should be optional, but db schema is not nullable - typeof data.website === 'string' && - Object.values(RepresentativeIDType).includes( - data.representativeIdType as RepresentativeIDType - ) && - typeof data.registrationCertificateFileId === 'number' && - typeof data.taxCertificateFileId === 'number' && - typeof data.representativeIdCardFileId === 'number' && - typeof data.createdAt === 'number' && - typeof data.updatedAt === 'number' && - (typeof data.deletedAt === 'number' || data.deletedAt === null) - ); +export function isCompanyKYC(data: unknown): data is ICompanyKYC { + // Deprecated: (20240912 - Murky) Use zod validator instead + // return ( + // typeof data.id === 'number' && + // typeof data.companyId === 'number' && + // typeof data.legalName === 'string' && + // Object.values(CountryOptions).includes(data.country as CountryOptions) && + // typeof data.city === 'string' && + // typeof data.address === 'string' && + // typeof data.zipCode === 'string' && + // typeof data.representativeName === 'string' && + // Object.values(LegalStructureOptions).includes(data.structure as LegalStructureOptions) && + // typeof data.registrationNumber === 'string' && + // typeof data.registrationDate === 'string' && + // Object.values(IndustryOptions).includes(data.industry as IndustryOptions) && + // typeof data.contactPerson === 'string' && + // typeof data.contactPhone === 'string' && + // typeof data.contactEmail === 'string' && + // // (typeof data.website === 'string' || data.website === undefined) && Info: (20240719 - Tzuhan) this field should be optional, but db schema is not nullable + // typeof data.website === 'string' && + // Object.values(RepresentativeIDType).includes( + // data.representativeIdType as RepresentativeIDType + // ) && + // typeof data.registrationCertificateFileId === 'number' && + // typeof data.taxCertificateFileId === 'number' && + // typeof data.representativeIdCardFileId === 'number' && + // typeof data.createdAt === 'number' && + // typeof data.updatedAt === 'number' && + // (typeof data.deletedAt === 'number' || data.deletedAt === null) + // ); + + const isValid = iCompanyKYCValidator.safeParse(data); + return isValid.success; } -export function isCompanyKYCForm(obj: ICompanyKYCForm): obj is ICompanyKYCForm { - const countryEnumValue = getEnumValue(CountryOptions, obj.country); - const structureEnumValue = getEnumValue(LegalStructureOptions, obj.structure); - const industryEnumValue = getEnumValue(IndustryOptions, obj.industry); - const representativeIdTypeEnumValue = getEnumValue( - RepresentativeIDType, - obj.representativeIdType - ); - return ( - typeof obj === 'object' && - !!countryEnumValue && - !!structureEnumValue && - !!industryEnumValue && - !!representativeIdTypeEnumValue && - typeof obj[BasicInfoKeys.LEGAL_COMPANY_NAME] === 'string' && - typeof obj[BasicInfoKeys.CITY] === 'string' && - typeof obj[BasicInfoKeys.ZIP_CODE] === 'string' && - typeof obj[BasicInfoKeys.ADDRESS] === 'string' && - typeof obj[BasicInfoKeys.KEY_COMPANY_REPRESENTATIVES_NAME] === 'string' && - typeof obj[RegistrationInfoKeys.LEGAL_STRUCTURE] === 'string' && - typeof obj[RegistrationInfoKeys.BUSINESS_REGISTRATION_NUMBER] === 'string' && - typeof obj[RegistrationInfoKeys.REGISTRATION_DATE] === 'string' && - typeof obj[RegistrationInfoKeys.INDUSTRY] === 'string' && - typeof obj[ContactInfoKeys.KEY_CONTACT_PERSON] === 'string' && - typeof obj[ContactInfoKeys.CONTACT_PHONE] === 'string' && - typeof obj[ContactInfoKeys.EMAIL_ADDRESS] === 'string' && - typeof obj[ContactInfoKeys.COMPANY_WEBSITE] === 'string' && - typeof obj[UploadDocumentKeys.REPRESENTATIVE_ID_TYPE] === 'string' && - typeof obj.registrationCertificateFileId === 'number' && - typeof obj.taxCertificateFileId === 'number' && - typeof obj.representativeIdCardFileId === 'number' - ); +export function isCompanyKYCForm(obj: unknown): obj is ICompanyKYCForm { + // Deprecated: (20240912 - Murky) Use zod validator instead + // const countryEnumValue = getEnumValue(CountryOptions, obj.country); + // const structureEnumValue = getEnumValue(LegalStructureOptions, obj.structure); + // const industryEnumValue = getEnumValue(IndustryOptions, obj.industry); + // const representativeIdTypeEnumValue = getEnumValue( + // RepresentativeIDType, + // obj.representativeIdType + // ); + // return ( + // typeof obj === 'object' && + // !!countryEnumValue && + // !!structureEnumValue && + // !!industryEnumValue && + // !!representativeIdTypeEnumValue && + // typeof obj[BasicInfoKeys.LEGAL_COMPANY_NAME] === 'string' && + // typeof obj[BasicInfoKeys.CITY] === 'string' && + // typeof obj[BasicInfoKeys.ZIP_CODE] === 'string' && + // typeof obj[BasicInfoKeys.ADDRESS] === 'string' && + // typeof obj[BasicInfoKeys.KEY_COMPANY_REPRESENTATIVES_NAME] === 'string' && + // typeof obj[RegistrationInfoKeys.LEGAL_STRUCTURE] === 'string' && + // typeof obj[RegistrationInfoKeys.BUSINESS_REGISTRATION_NUMBER] === 'string' && + // typeof obj[RegistrationInfoKeys.REGISTRATION_DATE] === 'string' && + // typeof obj[RegistrationInfoKeys.INDUSTRY] === 'string' && + // typeof obj[ContactInfoKeys.KEY_CONTACT_PERSON] === 'string' && + // typeof obj[ContactInfoKeys.CONTACT_PHONE] === 'string' && + // typeof obj[ContactInfoKeys.EMAIL_ADDRESS] === 'string' && + // typeof obj[ContactInfoKeys.COMPANY_WEBSITE] === 'string' && + // typeof obj[UploadDocumentKeys.REPRESENTATIVE_ID_TYPE] === 'string' && + // typeof obj.registrationCertificateFileId === 'number' && + // typeof obj.taxCertificateFileId === 'number' && + // typeof obj.representativeIdCardFileId === 'number' + // ); + + const isValid = iCompanyKYCFormValidator.safeParse(obj); + return isValid.success; } export function isKYCFormComplete(data: ICompanyKYCForm): { diff --git a/src/lib/utils/zod_schema/kyc.ts b/src/lib/utils/zod_schema/kyc.ts new file mode 100644 index 000000000..ce8b30022 --- /dev/null +++ b/src/lib/utils/zod_schema/kyc.ts @@ -0,0 +1,76 @@ +import { + BasicInfoKeys, + ContactInfoKeys, + CountryOptions, + IndustryOptions, + LegalStructureOptions, + RegistrationInfoKeys, + RepresentativeIDType, + UploadDocumentKeys, +} from '@/constants/kyc'; +import { z } from 'zod'; +import { IZodValidator } from '@/interfaces/zod_validator'; +import { zodTimestampInSeconds } from './common'; + +export const iCompanyKYCFormValidator = z.object({ + [BasicInfoKeys.LEGAL_COMPANY_NAME]: z.string(), + [BasicInfoKeys.CITY]: z.string(), + [BasicInfoKeys.ZIP_CODE]: z.string(), + [BasicInfoKeys.ADDRESS]: z.string(), + [BasicInfoKeys.KEY_COMPANY_REPRESENTATIVES_NAME]: z.string(), + [RegistrationInfoKeys.COUNTRY]: z.nativeEnum(CountryOptions), + [RegistrationInfoKeys.LEGAL_STRUCTURE]: z.nativeEnum(LegalStructureOptions), + [RegistrationInfoKeys.BUSINESS_REGISTRATION_NUMBER]: z.string(), + [RegistrationInfoKeys.REGISTRATION_DATE]: zodTimestampInSeconds(false).transform(String), // Info: (20240912 - Murky) Date in second but store in string + [RegistrationInfoKeys.INDUSTRY]: z.nativeEnum(IndustryOptions), + [ContactInfoKeys.KEY_CONTACT_PERSON]: z.string(), + [ContactInfoKeys.CONTACT_PHONE]: z.string(), + [ContactInfoKeys.EMAIL_ADDRESS]: z.string().email(), + [ContactInfoKeys.COMPANY_WEBSITE]: z.string().optional(), + [UploadDocumentKeys.REPRESENTATIVE_ID_TYPE]: z.nativeEnum(RepresentativeIDType), + [UploadDocumentKeys.BUSINESS_REGISTRATION_CERTIFICATE_ID]: z.number(), + [UploadDocumentKeys.TAX_STATUS_CERTIFICATE_ID]: z.number(), + [UploadDocumentKeys.REPRESENTATIVE_CERTIFICATE_ID]: z.number(), +}); + +const kycUploadQueryValidator = z.object({}); + +const kycUploadBodyValidator = iCompanyKYCFormValidator; + +export const kycUploadValidator: IZodValidator< + (typeof kycUploadQueryValidator)['shape'], + (typeof kycUploadBodyValidator)['shape'] +> = { + query: kycUploadQueryValidator, + body: kycUploadBodyValidator, +}; + +export const iCompanyKYCValidator = z.object({ + id: z.number(), + companyId: z.number(), + legalName: z.string(), + country: z.nativeEnum(CountryOptions), + city: z.string(), + address: z.string(), + zipCode: z.string(), + representativeName: z.string(), + structure: z.nativeEnum(LegalStructureOptions), + registrationNumber: z.string(), + registrationDate: z.string(), + industry: z.nativeEnum(IndustryOptions), + contactPerson: z.string(), + contactPhone: z.string(), + contactEmail: z.string().email(), + website: z.string(), + representativeIdType: z.nativeEnum(RepresentativeIDType), + registrationCertificateFileId: z.number(), + taxCertificateFileId: z.number(), + representativeIdCardFileId: z.number(), + status: z.string(), + reviewer: z.string(), + note: z.string(), + reviewAt: z.number(), + createdAt: z.number(), + updatedAt: z.number(), + deletedAt: z.number().nullable(), +}); diff --git a/src/pages/api/v1/company/[companyId]/kyc/index.ts b/src/pages/api/v1/company/[companyId]/kyc/index.ts index 04ad3bfb9..3b5322ad1 100644 --- a/src/pages/api/v1/company/[companyId]/kyc/index.ts +++ b/src/pages/api/v1/company/[companyId]/kyc/index.ts @@ -2,12 +2,15 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { IResponseData } from '@/interfaces/response_data'; import { formatApiResponse } from '@/lib/utils/common'; import { STATUS_MESSAGE } from '@/constants/status_code'; -import { ICompanyKYC, ICompanyKYCForm } from '@/interfaces/company_kyc'; +import { ICompanyKYC } from '@/interfaces/company_kyc'; import { getSession } from '@/lib/utils/session'; import { checkAuthorization } from '@/lib/utils/auth_check'; import { AuthFunctionsKeys } from '@/interfaces/auth'; import { createCompanyKYC } from '@/lib/utils/repo/company_kyc.repo'; import { isCompanyKYC, isCompanyKYCForm } from '@/lib/utils/type_guard/company_kyc'; +import { validateRequest } from '@/lib/utils/request_validator'; +import { APIName } from '@/constants/api_connection'; +import loggerBack, { loggerError } from '@/lib/utils/logger_back'; async function handlePostRequest( req: NextApiRequest, @@ -26,7 +29,8 @@ async function handlePostRequest( if (!isAuth) { statusMessage = STATUS_MESSAGE.FORBIDDEN; } else { - const companyKYCForm: ICompanyKYCForm = req.body; + const { body: companyKYCForm } = validateRequest(APIName.KYC_UPLOAD, req, userId); + loggerBack.info({ userId, companyId, companyKYCForm }); if (!isCompanyKYCForm(companyKYCForm)) { statusMessage = STATUS_MESSAGE.INVALID_INPUT_PARAMETER; } else { @@ -39,6 +43,12 @@ async function handlePostRequest( statusMessage = STATUS_MESSAGE.CREATED; } } catch (error) { + const logger = loggerError( + userId, + 'post /api/v1/company/[companyId]/kyc', + 'Failed to create company KYC' + ); + logger.error(error); statusMessage = STATUS_MESSAGE.INTERNAL_SERVICE_ERROR; } }