From da769523c82f6218c9681f59976dd1a043c4de1b Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Thu, 15 Aug 2024 17:51:14 +0800 Subject: [PATCH 01/15] connect to gemini aich --- package.json | 2 +- src/constants/aich.ts | 4 ++++ src/lib/utils/aich.ts | 17 +++++++++++++++++ .../company/[companyId]/ocr/[resultId]/index.ts | 6 ++++-- .../api/v1/company/[companyId]/ocr/index.ts | 4 ++-- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8d83c2ddc..b2693f385 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.0+7", + "version": "0.8.0+8", "private": false, "scripts": { "dev": "next dev", diff --git a/src/constants/aich.ts b/src/constants/aich.ts index b75e37cf8..78362b69a 100644 --- a/src/constants/aich.ts +++ b/src/constants/aich.ts @@ -1,4 +1,8 @@ export enum AICH_APIS_TYPES { UPLOAD_OCR = 'upload_ocr', GET_OCR_RESULT_ID = 'get_ocr_result_id', + GET_OCR_RESULT = 'get_ocr_result', + UPLOAD_GEMINI = 'upload_gemini', + GET_GEMINI_RESULT_ID = 'get_gemini_result_id', + GET_GEMINI_RESULT = 'get_gemini_result', } diff --git a/src/lib/utils/aich.ts b/src/lib/utils/aich.ts index a38a3bf03..624c48457 100644 --- a/src/lib/utils/aich.ts +++ b/src/lib/utils/aich.ts @@ -18,6 +18,23 @@ export function getAichUrl(endPoint: AICH_APIS_TYPES, aichResultId?: string): st throw new Error('AICH Result ID is required'); } return `${AICH_URI}/api/v1/ocr/${aichResultId}/process_status`; + case AICH_APIS_TYPES.GET_OCR_RESULT: + if (!aichResultId) { + throw new Error('AICH Result ID is required'); + } + return `${AICH_URI}/api/v1/ocr/${aichResultId}/result`; + case AICH_APIS_TYPES.UPLOAD_GEMINI: + return `${AICH_URI}/api/v1/gemini/upload`; + case AICH_APIS_TYPES.GET_GEMINI_RESULT_ID: + if (!aichResultId) { + throw new Error('AICH Result ID is required'); + } + return `${AICH_URI}/api/v1/gemini/${aichResultId}/process_status`; + case AICH_APIS_TYPES.GET_GEMINI_RESULT: + if (!aichResultId) { + throw new Error('AICH Result ID is required'); + } + return `${AICH_URI}/api/v1/gemini/${aichResultId}/result`; default: throw new Error('Invalid AICH API Type'); } diff --git a/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.ts b/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.ts index 4ee0be301..e3cdc8941 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.ts @@ -1,6 +1,5 @@ // Info Murky (20240416): this is mock api need to migrate to microservice import type { NextApiRequest, NextApiResponse } from 'next'; -import { AICH_URI } from '@/constants/config'; import { IResponseData } from '@/interfaces/response_data'; import { IInvoice } from '@/interfaces/invoice'; import { @@ -17,6 +16,8 @@ import { ProgressStatus } from '@/constants/account'; import { getSession } from '@/lib/utils/session'; import { checkAuthorization } from '@/lib/utils/auth_check'; import { AuthFunctionsKeys } from '@/interfaces/auth'; +import { getAichUrl } from '@/lib/utils/aich'; +import { AICH_APIS_TYPES } from '@/constants/aich'; // Info (20240522 - Murky): This OCR now can only be used on Invoice @@ -31,7 +32,8 @@ export async function fetchOCRResult(resultId: string) { let response: Response; try { - response = await fetch(`${AICH_URI}/api/v1/ocr/${resultId}/result`); + const fetchURL = getAichUrl(AICH_APIS_TYPES.GET_GEMINI_RESULT, resultId); + response = await fetch(fetchURL); } catch (error) { throw new Error(STATUS_MESSAGE.INTERNAL_SERVICE_ERROR_AICH_FAILED); } diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.ts b/src/pages/api/v1/company/[companyId]/ocr/index.ts index 613e2ac0c..6b947292b 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.ts @@ -57,7 +57,7 @@ export async function uploadImageToAICH(imageBlob: Blob, imageName: string) { const formData = createImageFormData(imageBlob, imageName); let response: Response; - const uploadUrl = getAichUrl(AICH_APIS_TYPES.UPLOAD_OCR); + const uploadUrl = getAichUrl(AICH_APIS_TYPES.UPLOAD_GEMINI); try { response = await fetch(uploadUrl, { method: 'POST', @@ -203,7 +203,7 @@ export async function fetchStatus(aichResultId: string) { if (aichResultId.length > 0) { try { - const fetchUrl = getAichUrl(AICH_APIS_TYPES.GET_OCR_RESULT_ID, aichResultId); + const fetchUrl = getAichUrl(AICH_APIS_TYPES.GET_GEMINI_RESULT_ID, aichResultId); const result = await fetch(fetchUrl); if (!result.ok) { From 136c51ef8a61d6cb4bc55c23b7285407e38a5fd0 Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Fri, 16 Aug 2024 10:28:14 +0800 Subject: [PATCH 02/15] change test case --- .../api/v1/company/[companyId]/ocr/[resultId]/index.test.ts | 2 +- src/pages/api/v1/company/[companyId]/ocr/index.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.test.ts b/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.test.ts index f8f2c69c1..53ee70f49 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.test.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.test.ts @@ -58,7 +58,7 @@ describe('fetchOCRResult', () => { const result = await module.fetchOCRResult(resultId); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining(`/api/v1/ocr/${resultId}/result`) + expect.stringContaining(`/api/v1/gemini/${resultId}/result`) ); expect(result).toEqual({ payload: 'testPayload' }); diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.test.ts b/src/pages/api/v1/company/[companyId]/ocr/index.test.ts index da8b44bfe..5262dea54 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.test.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.test.ts @@ -123,7 +123,7 @@ describe('POST OCR', () => { expect(promiseJson).toBeInstanceOf(Promise); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/ocr/upload'), + expect.stringContaining('/gemini/upload'), expect.objectContaining({ method: 'POST', body: expect.any(FormData) }) ); }); @@ -227,7 +227,7 @@ describe('POST OCR', () => { expect(resultJson).toEqual(resultJsonArrayExpect); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/ocr/upload'), + expect.stringContaining('/gemini/upload'), expect.objectContaining({ method: 'POST', body: expect.any(FormData) }) ); }); From d5029e163550b035e04eb9eb1644db09b6eade14 Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Fri, 16 Aug 2024 14:12:30 +0800 Subject: [PATCH 03/15] change ocr to feat front end --- package.json | 2 +- src/lib/utils/common.ts | 24 +++++ src/lib/utils/repo/ocr.repo.ts | 8 +- .../v1/company/[companyId]/ocr/index.test.ts | 52 +++++++-- .../api/v1/company/[companyId]/ocr/index.ts | 100 +++++++++++++++--- 5 files changed, 156 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 1fe80b5fc..37ce78cf8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.0+11", + "version": "0.8.0+12", "private": false, "scripts": { "dev": "next dev", diff --git a/src/lib/utils/common.ts b/src/lib/utils/common.ts index cb222d7a1..1da5bd794 100644 --- a/src/lib/utils/common.ts +++ b/src/lib/utils/common.ts @@ -350,6 +350,30 @@ export function transformBytesToFileSizeString(bytes: number): string { return `${size} ${sizes[i]}`; } +/** + * Info: (20240816 Murky): Transform file size string to bytes, file size string format should be like '1.00 MB' + * @param sizeString + * @returns + */ +export function transformFileSizeStringToBytes(sizeString: string): number { + const regex = /^\d+(\.\d+)? (Bytes|KB|MB|GB|TB|PB|EB|ZB|YB)$/; + + let bytes = 0; + if (regex.test(sizeString)) { + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const [size, unit] = sizeString.split(' '); + + const sizeIndex = sizes.indexOf(unit); + if (sizeIndex === -1) { + throw new Error('Invalid file size unit'); + } + + bytes = parseFloat(size) * 1024 ** sizeIndex; + } + + return Math.round(bytes); +} + // page, limit to offset export function pageToOffset( page: number = DEFAULT_PAGE_START_AT, diff --git a/src/lib/utils/repo/ocr.repo.ts b/src/lib/utils/repo/ocr.repo.ts index b0602db2a..a8b2987bb 100644 --- a/src/lib/utils/repo/ocr.repo.ts +++ b/src/lib/utils/repo/ocr.repo.ts @@ -98,8 +98,9 @@ export async function createOcrInPrisma( const now = Date.now(); const nowTimestamp = timestampInSeconds(now); + let ocrData: Ocr | null = null; try { - const ocrData = await prisma.ocr.create({ + ocrData = await prisma.ocr.create({ data: { companyId, aichResultId: aichResult.resultStatus.resultId, @@ -112,14 +113,13 @@ export async function createOcrInPrisma( updatedAt: nowTimestamp, }, }); - - return ocrData; } catch (error) { // Deprecated (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log(error); - throw new Error(STATUS_MESSAGE.DATABASE_CREATE_FAILED_ERROR); } + + return ocrData; } export async function deleteOcrByResultId(aichResultId: string): Promise { diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.test.ts b/src/pages/api/v1/company/[companyId]/ocr/index.test.ts index 5262dea54..961961f66 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.test.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.test.ts @@ -11,6 +11,7 @@ import * as repository from '@/lib/utils/repo/ocr.repo'; import { Ocr } from '@prisma/client'; import { IAccountResultStatus } from '@/interfaces/accounting_account'; import * as authCheck from '@/lib/utils/auth_check'; +import { IOCR } from '@/interfaces/ocr'; global.fetch = jest.fn(); @@ -175,6 +176,11 @@ describe('POST OCR', () => { describe('postImageToAICH', () => { let mockImages: MockProxy>; let mockImage: MockProxy; + let mockImageFields: { + imageSize: number; + imageName: string; + uploadIdentifier: string; + }[]; const mockPath = '/test'; const mockMimetype = 'image/png'; const mockFileContent = Buffer.from('mock image content'); @@ -182,6 +188,7 @@ describe('POST OCR', () => { mockImage = mock(); mockImage.filepath = mockPath; mockImage.mimetype = mockMimetype; + mockImageFields = []; mockImage.size = 1000; jest.spyOn(fs.promises, 'readFile').mockResolvedValue(mockFileContent); jest.spyOn(common, 'transformOCRImageIDToURL').mockReturnValue('testImageUrl'); @@ -201,7 +208,8 @@ describe('POST OCR', () => { mockImages = mock>({ image: [], }); - const result = await module.postImageToAICH(mockImages); + mockImageFields = []; + const result = await module.postImageToAICH(mockImages, mockImageFields); expect(result).toEqual([]); }); @@ -213,7 +221,7 @@ describe('POST OCR', () => { (global.fetch as jest.Mock).mockResolvedValue(mockResponse); - const resultJson = await module.postImageToAICH(mockImages); + const resultJson = await module.postImageToAICH(mockImages, mockImageFields); const resultJsonExpect = expect.objectContaining({ resultStatus: expect.any(String), @@ -271,7 +279,11 @@ describe('POST OCR', () => { }, ], }; - mockFields = mock(); + mockFields = { + imageSize: ["1 MB"], + imageName: ['test.png'], + uploadIdentifier: ['test'], + }; }); it('should return image file', async () => { @@ -281,19 +293,28 @@ describe('POST OCR', () => { }; jest.spyOn(parseImageForm, 'parseForm').mockResolvedValue(mockReturn); - const imageFile = await module.getImageFileFromFormData(req); - expect(imageFile).toEqual(mockFiles); + const imageFile = await module.getImageFileAndFormFromFormData(req); + + expect(imageFile).toEqual(mockReturn); }); it('should return empty object when parseForm failed', async () => { jest.spyOn(parseImageForm, 'parseForm').mockRejectedValue(new Error('parseForm failed')); - const result = await module.getImageFileFromFormData(req); - expect(result).toEqual({}); + const result = await module.getImageFileAndFormFromFormData(req); + + const expectReturn = { + fields: {}, + files: {}, + }; + + expect(result).toEqual(expectReturn); }); }); describe('createOcrFromAichResults', () => { it('should return resultJson', async () => { + jest.spyOn(common, 'timestampInSeconds').mockReturnValue(0); + jest.spyOn(common, 'transformBytesToFileSizeString').mockReturnValue('1 MB'); const resultId = 'testResultId'; const companyId = 1; const mockAichReturn = [ @@ -306,6 +327,8 @@ describe('POST OCR', () => { imageName: 'testImageName', imageSize: 1024, type: 'invoice', + uploadIdentifier: 'test', + createAt: 0, }, ]; @@ -323,11 +346,18 @@ describe('POST OCR', () => { deletedAt: null, }; - const expectResult: IAccountResultStatus[] = [ + const expectResult: IOCR[] = [ { - resultId, - status: ProgressStatus.SUCCESS, - }, + aichResultId: "testResultId", + createdAt: 0, + id: 1, + imageName: "testImageName", + imageSize: "1 MB", + imageUrl: "testImageUrl", + progress: 0, + status: ProgressStatus.IN_PROGRESS, + uploadIdentifier: "test", + } ]; jest.spyOn(repository, 'createOcrInPrisma').mockResolvedValue(mockOcrDbResult); diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.ts b/src/pages/api/v1/company/[companyId]/ocr/index.ts index 6b947292b..0dd24c265 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.ts @@ -9,6 +9,7 @@ import { timestampInMilliSeconds, timestampInSeconds, transformBytesToFileSizeString, + transformFileSizeStringToBytes, transformOCRImageIDToURL, } from '@/lib/utils/common'; import { parseForm } from '@/lib/utils/parse_image_form'; @@ -106,13 +107,18 @@ export async function getPayloadFromResponseJSON( // Info (20240521-Murky) 回傳目前還是array 的型態,因為可能會有多張圖片一起上傳 // 上傳圖片的時候把每個圖片的欄位名稱都叫做"image" 就可以了 -export async function postImageToAICH(files: formidable.Files): Promise< +export async function postImageToAICH(files: formidable.Files, imageFields: { + imageSize: number; + imageName: string; + uploadIdentifier: string; + }[]): Promise< { resultStatus: IAccountResultStatus; imageName: string; imageUrl: string; imageSize: number; type: string; + uploadIdentifier: string; }[] > { let resultJson: { @@ -121,11 +127,16 @@ export async function postImageToAICH(files: formidable.Files): Promise< imageUrl: string; imageSize: number; type: string; + uploadIdentifier: string; }[] = []; if (files && files.image && files.image.length) { // Info (20240504 - Murky): 圖片會先被存在本地端,然後才讀取路徑後轉傳給AICH resultJson = await Promise.all( - files.image.map(async (image) => { + files.image.map(async (image, index) => { + const imageFieldsLength = imageFields.length; + const isIndexValid = index < imageFieldsLength; + + // Info (20240816 - Murky): 壞檔的Image會被標上特殊的resultId const defaultResultId = 'error-' + generateUUID; let result: { resultStatus: IAccountResultStatus; @@ -133,6 +144,7 @@ export async function postImageToAICH(files: formidable.Files): Promise< imageUrl: string; imageSize: number; type: string; + uploadIdentifier: string; } = { resultStatus: { status: ProgressStatus.IN_PROGRESS, @@ -142,10 +154,12 @@ export async function postImageToAICH(files: formidable.Files): Promise< imageName: '', imageSize: 0, type: 'invoice', + uploadIdentifier: '', }; try { const imageBlob = await readImageFromFilePath(image); - const imageName = getImageName(image); + const imageName = isIndexValid ? imageFields[index].imageName : getImageName(image); + const imageSize = isIndexValid ? imageFields[index].imageSize : image.size; const fetchResult = uploadImageToAICH(imageBlob, imageName); @@ -155,8 +169,9 @@ export async function postImageToAICH(files: formidable.Files): Promise< resultStatus, imageUrl, imageName, - imageSize: image.size, + imageSize, type: 'invoice', + uploadIdentifier: isIndexValid ? imageFields[index].uploadIdentifier : '', }; } catch (error) { // Deprecated (20240611 - Murky) Debugging purpose @@ -184,20 +199,52 @@ export function isCompanyIdValid(companyId: any): companyId is number { return true; } -export async function getImageFileFromFormData(req: NextApiRequest) { +export function extractDataFromFields(fields: formidable.Fields) { + const { imageSize, imageName, uploadIdentifier } = fields; + + const imageFieldsArray: { + imageSize: number; + imageName: string; + uploadIdentifier: string; + }[] = []; + + if ( + imageSize && imageSize.length && + imageName && imageName.length && + uploadIdentifier && uploadIdentifier.length && + imageSize.length === imageName.length && imageSize.length === uploadIdentifier.length + ) { + imageSize.forEach((size, index) => { + imageFieldsArray.push({ + imageSize: transformFileSizeStringToBytes(size), + imageName: imageName[index], + uploadIdentifier: uploadIdentifier[index], + }); + }); + } + + // Info (20240815 - Murky) imageSize is string + return imageFieldsArray; +} + +export async function getImageFileAndFormFromFormData(req: NextApiRequest) { let files: formidable.Files = {}; + let fields: formidable.Fields = {}; try { const parsedForm = await parseForm(req, FileFolder.INVOICE); files = parsedForm.files; + fields = parsedForm.fields; } catch (error) { // Deprecated (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log(error); } - return files; + return { + files, + fields + }; } - export async function fetchStatus(aichResultId: string) { let status: ProgressStatus = ProgressStatus.SYSTEM_ERROR; @@ -275,15 +322,17 @@ export async function createOcrFromAichResults( imageName: string; imageSize: number; type: string; + uploadIdentifier: string; }[] ) { - const resultJson: IAccountResultStatus[] = []; + const resultJson: IOCR[] = []; + const ocrData: (Ocr | null)[] = []; try { await Promise.all( aichResults.map(async (aichResult) => { - await createOcrInPrisma(companyId, aichResult); - resultJson.push(aichResult.resultStatus); + const ocr = await createOcrInPrisma(companyId, aichResult); + ocrData.push(ocr); }) ); } catch (error) { @@ -292,15 +341,38 @@ export async function createOcrFromAichResults( console.log(error); throw new Error(STATUS_MESSAGE.DATABASE_CREATE_FAILED_ERROR); } + + aichResults.forEach((aichResult, index) => { + const ocr = ocrData[index]; + if (ocr) { + const imageSize = transformBytesToFileSizeString(aichResult.imageSize); + const createdAt = timestampInSeconds(ocr.createdAt); + const unprocessedOCR: IOCR = { + id: ocr.id, + aichResultId: ocr.aichResultId, + imageUrl: aichResult.imageUrl, + imageName: aichResult.imageName, + imageSize, + status: ProgressStatus.IN_PROGRESS, + progress: 0, + createdAt, + uploadIdentifier: aichResult.uploadIdentifier, + }; + + resultJson.push(unprocessedOCR); + } + }); + return resultJson; } export async function handlePostRequest(companyId: number, req: NextApiRequest) { - let resultJson: IAccountResultStatus[] = []; + let resultJson: IOCR[] = []; try { - const files = await getImageFileFromFormData(req); - const aichResults = await postImageToAICH(files); + const { files, fields } = await getImageFileAndFormFromFormData(req); + const imageFieldsArray = extractDataFromFields(fields); + const aichResults = await postImageToAICH(files, imageFieldsArray); // Deprecated (20240611 - Murky) This function is not used // resultJson = await createJournalsAndOcrFromAichResults(companyIdNumber, aichResults); resultJson = await createOcrFromAichResults(companyId, aichResults); @@ -335,7 +407,7 @@ export async function handleGetRequest(companyId: number, req: NextApiRequest) { return unprocessedOCRs; } -type ApiReturnType = IAccountResultStatus[] | IOCR[]; +type ApiReturnType = IOCR[]; export default async function handler( req: NextApiRequest, From 3c7b174137e11ce210d3a7660570531b02740f48 Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Fri, 16 Aug 2024 14:54:29 +0800 Subject: [PATCH 04/15] Update login_confirm_modal.beta.tsx --- src/components/login_confirm_modal/login_confirm_modal.beta.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/login_confirm_modal/login_confirm_modal.beta.tsx b/src/components/login_confirm_modal/login_confirm_modal.beta.tsx index f51020974..e2a5c033c 100644 --- a/src/components/login_confirm_modal/login_confirm_modal.beta.tsx +++ b/src/components/login_confirm_modal/login_confirm_modal.beta.tsx @@ -32,7 +32,7 @@ const LoginConfirmModal: React.FC = ({
-
+
{modalData.content === 'info_collection_statement' && } {modalData.content === 'term_n_privacy' && }
From 76d3acfd0c51251cb1a05bfd13d47abfc3fa49eb Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Fri, 16 Aug 2024 14:55:13 +0800 Subject: [PATCH 05/15] Update login_page_body.beta.tsx --- src/components/login_page_body/login_page_body.beta.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/login_page_body/login_page_body.beta.tsx b/src/components/login_page_body/login_page_body.beta.tsx index 529acd6a1..963155f2f 100644 --- a/src/components/login_page_body/login_page_body.beta.tsx +++ b/src/components/login_page_body/login_page_body.beta.tsx @@ -186,7 +186,7 @@ const LoginPageBody = () => { return (
-
+
{isLoading ? ( ) : ( From 0601bc36804bcd458493f71fcb2aba668aaae260 Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Fri, 16 Aug 2024 14:56:21 +0800 Subject: [PATCH 06/15] Update global_context.tsx --- src/contexts/global_context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contexts/global_context.tsx b/src/contexts/global_context.tsx index 3775f9a5f..0ffd04fe4 100644 --- a/src/contexts/global_context.tsx +++ b/src/contexts/global_context.tsx @@ -420,7 +420,7 @@ export const GlobalProvider = ({ children }: IGlobalProvider) => { const position = toastPosition ?? ToastPosition.TOP_CENTER; // Info:(20240513 - Julian) default position 'top-center' // Info:(20240513 - Julian) 如果 closeable 為 false,則 autoClose、closeOnClick、draggable 都會被設為 false - const autoClose = closeable ? (isAutoClose ?? 5000) : false; // Info:(20240513 - Julian) default autoClose 5000ms + const autoClose = closeable ? isAutoClose ?? 5000 : false; // Info:(20240513 - Julian) default autoClose 5000ms const closeOnClick = closeable; // Info:(20240513 - Julian) default closeOnClick true const draggable = closeable; // Info:(20240513 - Julian) default draggable true From 3f4f105dd66a32da44a2aab82bf8487595b2c5e2 Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Fri, 16 Aug 2024 14:56:55 +0800 Subject: [PATCH 07/15] Update use_api.ts --- src/lib/hooks/use_api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/hooks/use_api.ts b/src/lib/hooks/use_api.ts index dd021df23..9a0ce8174 100644 --- a/src/lib/hooks/use_api.ts +++ b/src/lib/hooks/use_api.ts @@ -68,7 +68,7 @@ const useAPI = ( apiConfig: IAPIConfig, options: IAPIInput, triggerImmediately: boolean = false, - cancel?: boolean + cancel?: boolean, ): IAPIResponse => { const [success, setSuccess] = useState(undefined); const [code, setCode] = useState(undefined); From e55a9a7c90fdd8f4f74799204e1e69fa00aaf77d Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Fri, 16 Aug 2024 14:57:09 +0800 Subject: [PATCH 08/15] Update use_api_worker.ts --- src/lib/hooks/use_api_worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/hooks/use_api_worker.ts b/src/lib/hooks/use_api_worker.ts index 19724894a..c1f7c77b7 100644 --- a/src/lib/hooks/use_api_worker.ts +++ b/src/lib/hooks/use_api_worker.ts @@ -8,7 +8,7 @@ const useAPIWorker = ( apiConfig: IAPIConfig, options: IAPIInput, triggerImmediately: boolean = false, - cancel?: boolean + cancel?: boolean, ): IAPIResponse => { const [success, setSuccess] = useState(undefined); const [code, setCode] = useState(undefined); From 86d91d0e5e46ecda644ef29cddf836c0bb8f3904 Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Fri, 16 Aug 2024 15:30:19 +0800 Subject: [PATCH 09/15] fix invoice disappear --- src/pages/api/v1/company/[companyId]/ocr/index.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.ts b/src/pages/api/v1/company/[companyId]/ocr/index.ts index 0dd24c265..3ed3a93c2 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.ts @@ -158,13 +158,15 @@ export async function postImageToAICH(files: formidable.Files, imageFields: { }; try { const imageBlob = await readImageFromFilePath(image); - const imageName = isIndexValid ? imageFields[index].imageName : getImageName(image); + + const imageNameInLocal = getImageName(image); + const imageName = isIndexValid ? imageFields[index].imageName : imageNameInLocal; const imageSize = isIndexValid ? imageFields[index].imageSize : image.size; const fetchResult = uploadImageToAICH(imageBlob, imageName); const resultStatus: IAccountResultStatus = await getPayloadFromResponseJSON(fetchResult); - const imageUrl = transformOCRImageIDToURL('invoice', 0, imageName); + const imageUrl = transformOCRImageIDToURL('invoice', 0, imageNameInLocal); result = { resultStatus, imageUrl, @@ -413,9 +415,12 @@ export default async function handler( req: NextApiRequest, res: NextApiResponse> ) { - const session = await getSession(req, res); - const { userId, companyId } = session; - const isAuth = await checkAuthorization([AuthFunctionsKeys.admin], { userId, companyId }); + // const session = await getSession(req, res); + // const { userId, companyId } = session; + // const isAuth = await checkAuthorization([AuthFunctionsKeys.admin], { userId, companyId }); + + const companyId = 10000007; + const isAuth = true; let payload: ApiReturnType = []; let statusMessage: string = STATUS_MESSAGE.BAD_REQUEST; From c0eaa06d3f9e028efc57ebdeb3d38d1d2e30f4ad Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Fri, 16 Aug 2024 15:32:03 +0800 Subject: [PATCH 10/15] fix origin --- src/pages/api/v1/company/[companyId]/ocr/index.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.ts b/src/pages/api/v1/company/[companyId]/ocr/index.ts index 3ed3a93c2..b6dcd6ee4 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.ts @@ -415,12 +415,9 @@ export default async function handler( req: NextApiRequest, res: NextApiResponse> ) { - // const session = await getSession(req, res); - // const { userId, companyId } = session; - // const isAuth = await checkAuthorization([AuthFunctionsKeys.admin], { userId, companyId }); - - const companyId = 10000007; - const isAuth = true; + const session = await getSession(req, res); + const { userId, companyId } = session; + const isAuth = await checkAuthorization([AuthFunctionsKeys.admin], { userId, companyId }); let payload: ApiReturnType = []; let statusMessage: string = STATUS_MESSAGE.BAD_REQUEST; From 44488344be438a63ca0722da4818e14e66eb733b Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Mon, 19 Aug 2024 14:46:38 +0800 Subject: [PATCH 11/15] change aich import --- src/constants/aich.ts | 6 +++--- src/lib/utils/aich.ts | 12 ++++++------ src/lib/utils/common.ts | 3 ++- .../v1/company/[companyId]/ocr/[resultId]/index.ts | 2 +- src/pages/api/v1/company/[companyId]/ocr/index.ts | 6 +++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/constants/aich.ts b/src/constants/aich.ts index 78362b69a..d76284393 100644 --- a/src/constants/aich.ts +++ b/src/constants/aich.ts @@ -2,7 +2,7 @@ export enum AICH_APIS_TYPES { UPLOAD_OCR = 'upload_ocr', GET_OCR_RESULT_ID = 'get_ocr_result_id', GET_OCR_RESULT = 'get_ocr_result', - UPLOAD_GEMINI = 'upload_gemini', - GET_GEMINI_RESULT_ID = 'get_gemini_result_id', - GET_GEMINI_RESULT = 'get_gemini_result', + UPLOAD_INVOICE = 'upload_invoice', + GET_INVOICE_RESULT_ID = 'get_invoice_result_id', + GET_INVOICE_RESULT = 'get_invoice_result', } diff --git a/src/lib/utils/aich.ts b/src/lib/utils/aich.ts index 624c48457..f079db4b0 100644 --- a/src/lib/utils/aich.ts +++ b/src/lib/utils/aich.ts @@ -23,18 +23,18 @@ export function getAichUrl(endPoint: AICH_APIS_TYPES, aichResultId?: string): st throw new Error('AICH Result ID is required'); } return `${AICH_URI}/api/v1/ocr/${aichResultId}/result`; - case AICH_APIS_TYPES.UPLOAD_GEMINI: - return `${AICH_URI}/api/v1/gemini/upload`; - case AICH_APIS_TYPES.GET_GEMINI_RESULT_ID: + case AICH_APIS_TYPES.UPLOAD_INVOICE: + return `${AICH_URI}/api/v1/invoices/upload`; + case AICH_APIS_TYPES.GET_INVOICE_RESULT_ID: if (!aichResultId) { throw new Error('AICH Result ID is required'); } - return `${AICH_URI}/api/v1/gemini/${aichResultId}/process_status`; - case AICH_APIS_TYPES.GET_GEMINI_RESULT: + return `${AICH_URI}/api/v1/invoices/${aichResultId}/process_status`; + case AICH_APIS_TYPES.GET_INVOICE_RESULT: if (!aichResultId) { throw new Error('AICH Result ID is required'); } - return `${AICH_URI}/api/v1/gemini/${aichResultId}/result`; + return `${AICH_URI}/api/v1/invoices/${aichResultId}/result`; default: throw new Error('Invalid AICH API Type'); } diff --git a/src/lib/utils/common.ts b/src/lib/utils/common.ts index 1da5bd794..14976149c 100644 --- a/src/lib/utils/common.ts +++ b/src/lib/utils/common.ts @@ -659,5 +659,6 @@ export function throttle unknown>( } export function generateUUID(): string { - return Math.random().toString(36).substring(2, 12); + const randomUUID = Math.random().toString(36).substring(2, 12); + return randomUUID; } diff --git a/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.ts b/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.ts index e3cdc8941..01492c023 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.ts @@ -32,7 +32,7 @@ export async function fetchOCRResult(resultId: string) { let response: Response; try { - const fetchURL = getAichUrl(AICH_APIS_TYPES.GET_GEMINI_RESULT, resultId); + const fetchURL = getAichUrl(AICH_APIS_TYPES.GET_INVOICE_RESULT, resultId); response = await fetch(fetchURL); } catch (error) { throw new Error(STATUS_MESSAGE.INTERNAL_SERVICE_ERROR_AICH_FAILED); diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.ts b/src/pages/api/v1/company/[companyId]/ocr/index.ts index b6dcd6ee4..e944d975d 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.ts @@ -58,7 +58,7 @@ export async function uploadImageToAICH(imageBlob: Blob, imageName: string) { const formData = createImageFormData(imageBlob, imageName); let response: Response; - const uploadUrl = getAichUrl(AICH_APIS_TYPES.UPLOAD_GEMINI); + const uploadUrl = getAichUrl(AICH_APIS_TYPES.UPLOAD_INVOICE); try { response = await fetch(uploadUrl, { method: 'POST', @@ -137,7 +137,7 @@ export async function postImageToAICH(files: formidable.Files, imageFields: { const isIndexValid = index < imageFieldsLength; // Info (20240816 - Murky): 壞檔的Image會被標上特殊的resultId - const defaultResultId = 'error-' + generateUUID; + const defaultResultId = 'error-' + generateUUID(); let result: { resultStatus: IAccountResultStatus; imageName: string; @@ -252,7 +252,7 @@ export async function fetchStatus(aichResultId: string) { if (aichResultId.length > 0) { try { - const fetchUrl = getAichUrl(AICH_APIS_TYPES.GET_GEMINI_RESULT_ID, aichResultId); + const fetchUrl = getAichUrl(AICH_APIS_TYPES.GET_INVOICE_RESULT_ID, aichResultId); const result = await fetch(fetchUrl); if (!result.ok) { From c98444e5bceb006bb0b675e4089b9a4bcad2dba6 Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Mon, 19 Aug 2024 15:25:12 +0800 Subject: [PATCH 12/15] ocr complete --- package.json | 2 +- src/pages/api/v1/company/[companyId]/ocr/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e6d79a64..af84a6a33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.0+13", + "version": "0.8.0+14", "private": false, "scripts": { "dev": "next dev", diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.ts b/src/pages/api/v1/company/[companyId]/ocr/index.ts index e944d975d..bdb839b83 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.ts @@ -235,6 +235,7 @@ export async function getImageFileAndFormFromFormData(req: NextApiRequest) { try { const parsedForm = await parseForm(req, FileFolder.INVOICE); + files = parsedForm.files; fields = parsedForm.fields; } catch (error) { From cdcd05cbff5c858217af70bdad767d5d12fd557a Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Mon, 19 Aug 2024 15:35:32 +0800 Subject: [PATCH 13/15] change test case --- .../v1/company/[companyId]/ocr/[resultId]/index.test.ts | 2 +- src/pages/api/v1/company/[companyId]/ocr/index.test.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.test.ts b/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.test.ts index 53ee70f49..b2cb926df 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.test.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.test.ts @@ -58,7 +58,7 @@ describe('fetchOCRResult', () => { const result = await module.fetchOCRResult(resultId); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining(`/api/v1/gemini/${resultId}/result`) + expect.stringContaining(`/api/v1/invoices/${resultId}/result`) ); expect(result).toEqual({ payload: 'testPayload' }); diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.test.ts b/src/pages/api/v1/company/[companyId]/ocr/index.test.ts index 961961f66..8ea1e721c 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.test.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.test.ts @@ -25,6 +25,7 @@ jest.mock('../../../../../../lib/utils/common', () => ({ timestampInSeconds: jest.fn(), timestampInMilliSeconds: jest.fn(), transformBytesToFileSizeString: jest.fn(), + generateUUID: jest.fn(), })); jest.mock('../../../../../../lib/utils/repo/ocr.repo', () => { @@ -124,7 +125,7 @@ describe('POST OCR', () => { expect(promiseJson).toBeInstanceOf(Promise); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/gemini/upload'), + expect.stringContaining('/invoices/upload'), expect.objectContaining({ method: 'POST', body: expect.any(FormData) }) ); }); @@ -235,7 +236,7 @@ describe('POST OCR', () => { expect(resultJson).toEqual(resultJsonArrayExpect); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/gemini/upload'), + expect.stringContaining('/invoices/upload'), expect.objectContaining({ method: 'POST', body: expect.any(FormData) }) ); }); @@ -463,6 +464,7 @@ describe('GET OCR', () => { describe('fetchStatus', () => { it('should return resultJson', async () => { const aichResultId = 'testAichResultId'; + const mockResponse = { ok: true, json: jest.fn().mockResolvedValue({ payload: ProgressStatus.SUCCESS }), @@ -470,6 +472,8 @@ describe('GET OCR', () => { (global.fetch as jest.Mock).mockResolvedValue(mockResponse); + jest.spyOn(common, 'generateUUID').mockReturnValue(aichResultId); + const resultJson = await module.fetchStatus(aichResultId); expect(resultJson).toEqual(ProgressStatus.SUCCESS); }); From 123dfff475fc4c27dd85bf61304aa6b8d5212d28 Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Mon, 19 Aug 2024 19:25:44 +0800 Subject: [PATCH 14/15] Update index.ts --- .../api/v1/company/[companyId]/ocr/index.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.ts b/src/pages/api/v1/company/[companyId]/ocr/index.ts index bdb839b83..5aed0fc69 100644 --- a/src/pages/api/v1/company/[companyId]/ocr/index.ts +++ b/src/pages/api/v1/company/[companyId]/ocr/index.ts @@ -31,7 +31,7 @@ import { getAichUrl } from '@/lib/utils/aich'; import { AICH_APIS_TYPES } from '@/constants/aich'; import { AVERAGE_OCR_PROCESSING_TIME } from '@/constants/ocr'; -// Info Murky (20240424) 要使用formidable要先關掉bodyParser +// Info: (20240424 - Murky) 要使用formidable要先關掉bodyParser export const config = { api: { bodyParser: false, @@ -65,7 +65,7 @@ export async function uploadImageToAICH(imageBlob: Blob, imageName: string) { body: formData, }); } catch (error) { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log(error); throw new Error(STATUS_MESSAGE.INTERNAL_SERVICE_ERROR_AICH_FAILED); @@ -92,7 +92,7 @@ export async function getPayloadFromResponseJSON( try { json = await responseJSON; } catch (error) { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log(error); throw new Error(STATUS_MESSAGE.PARSE_JSON_FAILED_ERROR); @@ -105,7 +105,7 @@ export async function getPayloadFromResponseJSON( return json.payload as IAccountResultStatus; } -// Info (20240521-Murky) 回傳目前還是array 的型態,因為可能會有多張圖片一起上傳 +// Info: (20240521 - Murky) 回傳目前還是 array 的型態,因為可能會有多張圖片一起上傳 // 上傳圖片的時候把每個圖片的欄位名稱都叫做"image" 就可以了 export async function postImageToAICH(files: formidable.Files, imageFields: { imageSize: number; @@ -130,13 +130,13 @@ export async function postImageToAICH(files: formidable.Files, imageFields: { uploadIdentifier: string; }[] = []; if (files && files.image && files.image.length) { - // Info (20240504 - Murky): 圖片會先被存在本地端,然後才讀取路徑後轉傳給AICH + // Info: (20240504 - Murky) 圖片會先被存在本地端,然後才讀取路徑後轉傳給 AICH resultJson = await Promise.all( files.image.map(async (image, index) => { const imageFieldsLength = imageFields.length; const isIndexValid = index < imageFieldsLength; - // Info (20240816 - Murky): 壞檔的Image會被標上特殊的resultId + // Info: (20240816 - Murky) 壞檔的 Image 會被標上特殊的 resultId const defaultResultId = 'error-' + generateUUID(); let result: { resultStatus: IAccountResultStatus; @@ -176,7 +176,7 @@ export async function postImageToAICH(files: formidable.Files, imageFields: { uploadIdentifier: isIndexValid ? imageFields[index].uploadIdentifier : '', }; } catch (error) { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log(error); } @@ -184,7 +184,7 @@ export async function postImageToAICH(files: formidable.Files, imageFields: { }) ); } else { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log('No image file found in formidable when upload ocr'); } @@ -225,7 +225,7 @@ export function extractDataFromFields(fields: formidable.Fields) { }); } - // Info (20240815 - Murky) imageSize is string + // Info: (20240815 - Murky) imageSize is string return imageFieldsArray; } @@ -239,7 +239,7 @@ export async function getImageFileAndFormFromFormData(req: NextApiRequest) { files = parsedForm.files; fields = parsedForm.fields; } catch (error) { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log(error); } @@ -262,7 +262,7 @@ export async function fetchStatus(aichResultId: string) { status = (await result.json()).payload; } catch (error) { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log(error); throw new Error(STATUS_MESSAGE.INTERNAL_SERVICE_ERROR_AICH_FAILED); @@ -272,7 +272,7 @@ export async function fetchStatus(aichResultId: string) { return status; } -// Deprecated (20240809 - Murky) This function is not used +// Deprecated: (20240809 - Murky) This function is not used export function calculateProgress(createdAt: number, status: ProgressStatus, ocrResultId: string) { const currentTime = new Date(); const diffTime = currentTime.getTime() - timestampInMilliSeconds(createdAt); @@ -339,7 +339,7 @@ export async function createOcrFromAichResults( }) ); } catch (error) { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log(error); throw new Error(STATUS_MESSAGE.DATABASE_CREATE_FAILED_ERROR); @@ -376,11 +376,11 @@ export async function handlePostRequest(companyId: number, req: NextApiRequest) const { files, fields } = await getImageFileAndFormFromFormData(req); const imageFieldsArray = extractDataFromFields(fields); const aichResults = await postImageToAICH(files, imageFieldsArray); - // Deprecated (20240611 - Murky) This function is not used + // Deprecated: (20240611 - Murky) This function is not used // resultJson = await createJournalsAndOcrFromAichResults(companyIdNumber, aichResults); resultJson = await createOcrFromAichResults(companyId, aichResults); } catch (error) { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.error(error); } @@ -398,7 +398,7 @@ export async function handleGetRequest(companyId: number, req: NextApiRequest) { try { ocrData = await findManyOCRByCompanyIdWithoutUsedInPrisma(companyId, ocrType as string); } catch (error) { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.log(error); throw new Error(STATUS_MESSAGE.INTERNAL_SERVICE_ERROR); @@ -442,7 +442,7 @@ export default async function handler( } } catch (_error) { const error = _error as Error; - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.error(error); } From 5ee7d20dffbfc5351b005ae71d0d48a27fdec89b Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Mon, 19 Aug 2024 19:26:05 +0800 Subject: [PATCH 15/15] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af84a6a33..2e6d79a64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.0+14", + "version": "0.8.0+13", "private": false, "scripts": { "dev": "next dev",