Skip to content

Commit

Permalink
feat: 🎸 mock trial-balance and ledger APIs with zod and unit
Browse files Browse the repository at this point in the history
test

✅ Closes: #2641
  • Loading branch information
arealclimber committed Sep 26, 2024
1 parent 1da9730 commit 5a153e1
Show file tree
Hide file tree
Showing 12 changed files with 577 additions and 2 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iSunFA",
"version": "0.8.2+11",
"version": "0.8.2+13",
"private": false,
"scripts": {
"dev": "next dev",
Expand Down
14 changes: 14 additions & 0 deletions src/constants/api_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export enum APIName {
PUBLIC_KEY_GET = 'PUBLIC_KEY_GET',
ZOD_EXAMPLE = 'ZOD_EXAMPLE', // Info: (20240909 - Murky) This is a Zod example, to demonstrate how to use Zod schema to validate data.
CERTIFICATE_LIST = 'CERTIFICATE_LIST',
TRIAL_BALANCE_LIST_V2 = 'TRIAL_BALANCE_LIST_V2',
LEDGER_LIST_V2 = 'LEDGER_LIST_V2',
}

export enum APIPath {
Expand Down Expand Up @@ -165,6 +167,8 @@ export enum APIPath {
PUBLIC_KEY_GET = `${apiPrefix}/company/:companyId/public_key`,
ZOD_EXAMPLE = `${apiPrefix}/company/zod`, // Info: (20240909 - Murky) This is a Zod example, to demonstrate how to use Zod schema to validate data.
CERTIFICATE_LIST = `${apiPrefix}/company/:companyId/certificate`,
TRIAL_BALANCE_LIST_V2 = `${apiPrefixV2}/company/:companyId/trial_balance`,
LEDGER_LIST_V2 = `${apiPrefixV2}/company/:companyId/ledger`,
}
const createConfig = ({
name,
Expand Down Expand Up @@ -506,4 +510,14 @@ export const APIConfig: Record<IAPIName, IAPIConfig> = {
method: HttpMethod.GET,
path: APIPath.CERTIFICATE_LIST,
}),
[APIName.TRIAL_BALANCE_LIST_V2]: createConfig({
name: APIName.TRIAL_BALANCE_LIST_V2,
method: HttpMethod.GET,
path: APIPath.TRIAL_BALANCE_LIST_V2,
}),
[APIName.LEDGER_LIST_V2]: createConfig({
name: APIName.LEDGER_LIST_V2,
method: HttpMethod.GET,
path: APIPath.LEDGER_LIST_V2,
}),
};
5 changes: 5 additions & 0 deletions src/constants/zod_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import {
journalListValidator,
} from '@/lib/utils/zod_schema/journal';
import { kycUploadValidator } from '@/lib/utils/zod_schema/kyc';
import { ledgerListValidator } from '@/lib/utils/zod_schema/ledger';
import {
ocrDeleteValidator,
ocrListValidator,
ocrResultGetByIdValidator,
ocrUploadValidator,
} from '@/lib/utils/zod_schema/ocr';
import { trialBalanceListValidator } from '@/lib/utils/zod_schema/trial_balance';
import { voucherCreateValidator, voucherUpdateValidator } from '@/lib/utils/zod_schema/voucher';
import { zodExampleValidator } from '@/lib/utils/zod_schema/zod_example';

Expand Down Expand Up @@ -59,4 +61,7 @@ export const API_ZOD_SCHEMA = {
[APIName.CERTIFICATE_POST_V2]: certificatePostValidator,
[APIName.CERTIFICATE_PUT_V2]: certificatePutValidator,
[APIName.CERTIFICATE_DELETE_V2]: certificateDeleteValidator,

[APIName.TRIAL_BALANCE_LIST_V2]: trialBalanceListValidator,
[APIName.LEDGER_LIST_V2]: ledgerListValidator,
};
4 changes: 3 additions & 1 deletion src/interfaces/api_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export type IAPIName =
| 'GET_PROJECT_BY_ID'
| 'UPDATE_PROJECT_BY_ID'
| 'PUBLIC_KEY_GET'
| 'CERTIFICATE_LIST';
| 'CERTIFICATE_LIST'
| 'TRIAL_BALANCE_LIST_V2'
| 'LEDGER_LIST_V2';

export type IHttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';

Expand Down
60 changes: 60 additions & 0 deletions src/lib/utils/zod_schema/ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { IZodValidator } from '@/interfaces/zod_validator';
import { z } from 'zod';
import { zodStringToNumberWithDefault, zodTimestampInSeconds } from '@/lib/utils/zod_schema/common';
import { DEFAULT_PAGE_NUMBER } from '@/constants/display';
import { DEFAULT_PAGE_LIMIT } from '@/constants/config';
import { SortOrder } from '@/constants/sort';

const ledgerListQueryValidator = z.object({
page: zodStringToNumberWithDefault(DEFAULT_PAGE_NUMBER),
pageSize: zodStringToNumberWithDefault(DEFAULT_PAGE_LIMIT),
sortOrder: z.nativeEnum(SortOrder).optional(),
startDate: zodTimestampInSeconds(true, 0),
endDate: zodTimestampInSeconds(true, Infinity),
startAccountNo: z.string().optional(),
endAccountNo: z.string().optional(),
labelType: z.enum(['general', 'detailed', 'all']).optional(),
});

export const ledgerReturnValidator = z.object({
id: z.number(),
voucherDate: z.number(),
no: z.string(),
accountingTitle: z.string(),
voucherNumber: z.string(),
particulars: z.string(),
debitAmount: z.number(),
creditAmount: z.number(),
balance: z.number(),
createAt: z.number(),
updateAt: z.number(),
});

export const ledgerListReturnValidator = z.object({
currency: z.string(),
items: z.object({
data: z.array(ledgerReturnValidator),
page: z.number(),
totalPages: z.number(),
totalCount: z.number(),
pageSize: z.number(),
hasNextPage: z.boolean(),
hasPreviousPage: z.boolean(),
sort: z.array(
z.object({
sortBy: z.string(),
sortOrder: z.string(),
})
),
}),
totalDebitAmount: z.number(),
totalCreditAmount: z.number(),
});

export const ledgerListValidator: IZodValidator<
(typeof ledgerListQueryValidator)['shape'],
z.ZodOptional<z.ZodNullable<z.ZodString>>
> = {
query: ledgerListQueryValidator,
body: z.string().nullable().optional(),
};
53 changes: 53 additions & 0 deletions src/lib/utils/zod_schema/trial_balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { IZodValidator } from '@/interfaces/zod_validator';
import { z } from 'zod';
import { zodStringToNumberWithDefault, zodTimestampInSeconds } from '@/lib/utils/zod_schema/common';
import { DEFAULT_PAGE_NUMBER } from '@/constants/display';
import { DEFAULT_PAGE_LIMIT } from '@/constants/config';
import { SortOrder } from '@/constants/sort';

const trialBalanceListQueryValidator = z.object({
page: zodStringToNumberWithDefault(DEFAULT_PAGE_NUMBER),
pageSize: zodStringToNumberWithDefault(DEFAULT_PAGE_LIMIT),
sortOrder: z.nativeEnum(SortOrder).optional(),
startDate: zodTimestampInSeconds(true, 0),
endDate: zodTimestampInSeconds(true, Infinity),
});

const basicTrialBalanceReturnValidator = z.object({
id: z.number(),
no: z.string(),
accountingTitle: z.string(),
beginningCreditAmount: z.number(),
beginningDebitAmount: z.number(),
midtermCreditAmount: z.number(),
midtermDebitAmount: z.number(),
endingCreditAmount: z.number(),
endingDebitAmount: z.number(),
createAt: z.number(),
updateAt: z.number(),
});

export const trialBalanceReturnValidator = z.object({
id: z.number(),
no: z.string(),
accountingTitle: z.string(),
beginningCreditAmount: z.number(),
beginningDebitAmount: z.number(),
midtermCreditAmount: z.number(),
midtermDebitAmount: z.number(),
endingCreditAmount: z.number(),
endingDebitAmount: z.number(),
createAt: z.number(),
updateAt: z.number(),
subAccounts: z.array(basicTrialBalanceReturnValidator),
});

export const trialBalanceListReturnValidator = z.array(trialBalanceReturnValidator);

export const trialBalanceListValidator: IZodValidator<
(typeof trialBalanceListQueryValidator)['shape'],
z.ZodOptional<z.ZodNullable<z.ZodString>>
> = {
query: trialBalanceListQueryValidator,
body: z.string().nullable().optional(),
};
67 changes: 67 additions & 0 deletions src/pages/api/v2/company/[companyId]/ledger/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { handleGetRequest } from '@/pages/api/v2/company/[companyId]/ledger/index';
import { STATUS_MESSAGE } from '@/constants/status_code';

let req: jest.Mocked<NextApiRequest>;
let res: jest.Mocked<NextApiResponse>;

jest.mock('../../../../../../lib/utils/session.ts', () => ({
getSession: jest.fn().mockResolvedValue({
userId: 1001,
companyId: 1001,
}),
}));

jest.mock('../../../../../../lib/utils/request_validator', () => ({
validateRequest: jest.fn().mockReturnValue({
query: {
startDate: '2024-01-01',
endDate: '2024-12-31',
startAccountNo: '1000',
endAccountNo: '9999',
labelType: 'all',
page: 1,
pageSize: 10,
sortOrder: 'asc',
},
}),
}));

beforeEach(() => {
req = {
headers: {},
query: {},
method: '',
socket: {},
json: jest.fn(),
} as unknown as jest.Mocked<NextApiRequest>;

res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
} as unknown as jest.Mocked<NextApiResponse>;
});

afterEach(() => {
jest.clearAllMocks();
});

describe('company/[companyId]/ledger', () => {
describe('GET Ledger List', () => {
it('should return ledger list', async () => {
req.query = {
companyId: '1001',
};

const { payload, statusMessage } = await handleGetRequest(req, res);

expect(statusMessage).toBe(STATUS_MESSAGE.SUCCESS_LIST);
expect(payload).toHaveProperty('currency', 'TWD');
expect(payload).toHaveProperty('items');
expect(payload?.items).toHaveProperty('data');
expect(payload?.items.data).toBeInstanceOf(Array);
expect(payload).toHaveProperty('totalDebitAmount');
expect(payload).toHaveProperty('totalCreditAmount');
});
});
});
123 changes: 123 additions & 0 deletions src/pages/api/v2/company/[companyId]/ledger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { STATUS_MESSAGE } from '@/constants/status_code';
import { IResponseData } from '@/interfaces/response_data';
import { formatApiResponse } from '@/lib/utils/common';
import { getSession } from '@/lib/utils/session';
import { NextApiRequest, NextApiResponse } from 'next';
import { validateRequest } from '@/lib/utils/request_validator';
import { APIName } from '@/constants/api_connection';
import { mockLedgerList } from '@/pages/api/v2/company/[companyId]/ledger/service';

type APIResponse = {
currency: string;
items: {
data: Array<{
id: number;
voucherDate: number;
no: string;
accountingTitle: string;
voucherNumber: string;
particulars: string;
debitAmount: number;
creditAmount: number;
balance: number;
createAt: number;
updateAt: number;
}>;
page: number;
totalPages: number;
totalCount: number;
pageSize: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
sort: Array<{
sortBy: string;
sortOrder: string;
}>;
};
totalDebitAmount: number;
totalCreditAmount: number;
} | null;

export async function handleGetRequest(req: NextApiRequest, res: NextApiResponse<APIResponse>) {
let statusMessage: string = STATUS_MESSAGE.BAD_REQUEST;
let payload: APIResponse = null;

// TODO: 實作時確認 auth (20240926 - Shirley)
const session = await getSession(req, res);
const { userId } = session;

const { query } = validateRequest(APIName.LEDGER_LIST_V2, req, userId);

if (query) {
// TODO: 實作 API 時補上 (20240926 - Shirley)
/* eslint-disable @typescript-eslint/no-unused-vars */
const {
startDate,
endDate,
startAccountNo,
endAccountNo,
labelType,
page,
pageSize,
sortOrder,
} = query;

statusMessage = STATUS_MESSAGE.SUCCESS_LIST;
payload = {
currency: 'TWD',
items: {
data: mockLedgerList,
page: page ?? 1,
totalPages: 1,
totalCount: mockLedgerList.length,
pageSize: pageSize ?? mockLedgerList.length,
hasNextPage: false,
hasPreviousPage: false,
sort: [
{
sortBy: 'no',
sortOrder: 'asc',
},
],
},
totalDebitAmount: 2300000,
totalCreditAmount: 1300000,
};
}

return {
statusMessage,
payload,
};
}

const methodHandlers: {
[key: string]: (
req: NextApiRequest,
res: NextApiResponse
) => Promise<{ statusMessage: string; payload: APIResponse }>;
} = {
GET: handleGetRequest,
};

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<IResponseData<APIResponse>>
) {
let statusMessage: string = STATUS_MESSAGE.BAD_REQUEST;
let payload: APIResponse = null;

try {
const handleRequest = methodHandlers[req.method || ''];
if (handleRequest) {
({ statusMessage, payload } = await handleRequest(req, res));
} else {
statusMessage = STATUS_MESSAGE.METHOD_NOT_ALLOWED;
}
} catch (_error) {
const error = _error as Error;
statusMessage = error.message;
}
const { httpCode, result } = formatApiResponse<APIResponse>(statusMessage, payload);
res.status(httpCode).json(result);
}
Loading

0 comments on commit 5a153e1

Please sign in to comment.