diff --git a/package.json b/package.json index 37ce78cf8..2e6d79a64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.0+12", + "version": "0.8.0+13", "private": false, "scripts": { "dev": "next dev", diff --git a/src/constants/aich.ts b/src/constants/aich.ts index b75e37cf8..d76284393 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_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 a38a3bf03..f079db4b0 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_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/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/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 cb222d7a1..14976149c 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, @@ -635,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/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/[resultId]/index.test.ts b/src/pages/api/v1/company/[companyId]/ocr/[resultId]/index.test.ts index f8f2c69c1..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/ocr/${resultId}/result`) + expect.stringContaining(`/api/v1/invoices/${resultId}/result`) ); expect(result).toEqual({ payload: 'testPayload' }); 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..01492c023 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_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.test.ts b/src/pages/api/v1/company/[companyId]/ocr/index.test.ts index da8b44bfe..8ea1e721c 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(); @@ -24,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', () => { @@ -123,7 +125,7 @@ describe('POST OCR', () => { expect(promiseJson).toBeInstanceOf(Promise); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/ocr/upload'), + expect.stringContaining('/invoices/upload'), expect.objectContaining({ method: 'POST', body: expect.any(FormData) }) ); }); @@ -175,6 +177,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 +189,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 +209,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 +222,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), @@ -227,7 +236,7 @@ describe('POST OCR', () => { expect(resultJson).toEqual(resultJsonArrayExpect); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/ocr/upload'), + expect.stringContaining('/invoices/upload'), expect.objectContaining({ method: 'POST', body: expect.any(FormData) }) ); }); @@ -271,7 +280,11 @@ describe('POST OCR', () => { }, ], }; - mockFields = mock(); + mockFields = { + imageSize: ["1 MB"], + imageName: ['test.png'], + uploadIdentifier: ['test'], + }; }); it('should return image file', async () => { @@ -281,19 +294,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 +328,8 @@ describe('POST OCR', () => { imageName: 'testImageName', imageSize: 1024, type: 'invoice', + uploadIdentifier: 'test', + createAt: 0, }, ]; @@ -323,11 +347,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); @@ -433,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 }), @@ -440,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); }); diff --git a/src/pages/api/v1/company/[companyId]/ocr/index.ts b/src/pages/api/v1/company/[companyId]/ocr/index.ts index 613e2ac0c..5aed0fc69 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'; @@ -30,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, @@ -57,14 +58,14 @@ 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_INVOICE); try { response = await fetch(uploadUrl, { method: 'POST', 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); @@ -91,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); @@ -104,15 +105,20 @@ 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): 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,18 +127,24 @@ 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 + // Info: (20240504 - Murky) 圖片會先被存在本地端,然後才讀取路徑後轉傳給 AICH resultJson = await Promise.all( - files.image.map(async (image) => { - const defaultResultId = 'error-' + generateUUID; + 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; imageName: string; imageUrl: string; imageSize: number; type: string; + uploadIdentifier: string; } = { resultStatus: { status: ProgressStatus.IN_PROGRESS, @@ -142,24 +154,29 @@ 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 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, imageName, - imageSize: image.size, + imageSize, type: 'invoice', + 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); } @@ -167,7 +184,7 @@ export async function postImageToAICH(files: formidable.Files): Promise< }) ); } 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'); } @@ -184,26 +201,59 @@ 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 + // 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; if (aichResultId.length > 0) { try { - const fetchUrl = getAichUrl(AICH_APIS_TYPES.GET_OCR_RESULT_ID, aichResultId); + const fetchUrl = getAichUrl(AICH_APIS_TYPES.GET_INVOICE_RESULT_ID, aichResultId); const result = await fetch(fetchUrl); if (!result.ok) { @@ -212,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); @@ -222,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); @@ -275,37 +325,62 @@ 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) { - // 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); } + + 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); - // Deprecated (20240611 - Murky) This function is not used + 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); } catch (error) { - // Deprecated (20240611 - Murky) Debugging purpose + // Deprecated: (20240611 - Murky) Debugging purpose // eslint-disable-next-line no-console console.error(error); } @@ -323,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); @@ -335,7 +410,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, @@ -367,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); } diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index f5053d7a1..9bb8194eb 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -8,7 +8,7 @@ declare module 'next-auth' { user: { id: string; hasReadAgreement: boolean; - } & DefaultSession['user'] ; + } & DefaultSession['user']; } interface User {