diff --git a/lerna.json b/lerna.json index d49df65..43474b2 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "packages": ["packages/*"] } diff --git a/package-lock.json b/package-lock.json index 4d729b3..80fa5c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36035,7 +36035,7 @@ }, "packages/cli": { "name": "@mbc-cqrs-serverless/cli", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "license": "MIT", "dependencies": { "commander": "^11.1.0", @@ -36075,7 +36075,7 @@ }, "packages/core": { "name": "@mbc-cqrs-serverless/core", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "license": "MIT", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", @@ -36164,26 +36164,26 @@ }, "packages/sequence": { "name": "@mbc-cqrs-serverless/sequence", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "license": "MIT", "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.33-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.35-beta.0" } }, "packages/task": { "name": "@mbc-cqrs-serverless/task", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "license": "MIT", "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.33-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.35-beta.0" } }, "packages/ui-setting": { "name": "@mbc-cqrs-serverless/ui-setting", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "license": "MIT", "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.33-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.35-beta.0" } } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 49e289b..ec93323 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/cli", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "description": "a CLI to get started with MBC CQRS serverless framework", "keywords": [ "mbc", diff --git a/packages/core/package.json b/packages/core/package.json index dcdf254..c14341a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/core", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "description": "CQRS and event base core", "keywords": [ "mbc", diff --git a/packages/core/src/constants/tenant.ts b/packages/core/src/constants/tenant.ts index ab02853..1f4b47c 100644 --- a/packages/core/src/constants/tenant.ts +++ b/packages/core/src/constants/tenant.ts @@ -1,2 +1,3 @@ export const HEADER_TENANT_CODE = 'x-tenant-code' export const TENANT_COMMON = 'common' +export const DEFAULT_TENANT_CODE = 'single' diff --git a/packages/core/src/helpers/key.ts b/packages/core/src/helpers/key.ts index 6ed4cfb..eb40484 100644 --- a/packages/core/src/helpers/key.ts +++ b/packages/core/src/helpers/key.ts @@ -1,3 +1,4 @@ +import { DEFAULT_TENANT_CODE } from '../constants' import { KEY_SEPARATOR, VER_SEPARATOR } from '../constants/key' export function addSortKeyVersion(sk: string, version: number) { @@ -40,3 +41,9 @@ export function parseS3AttributeKey(s3Uri: string) { key, } } + +export const masterPk = (tenantCode?: string) => + `MASTER${KEY_SEPARATOR}${tenantCode || DEFAULT_TENANT_CODE}` + +export const seqPk = (tenantCode?: string) => + `SEQ${KEY_SEPARATOR}${tenantCode || DEFAULT_TENANT_CODE}` diff --git a/packages/core/src/interfaces/index.ts b/packages/core/src/interfaces/index.ts index 636ca89..f7b96e5 100644 --- a/packages/core/src/interfaces/index.ts +++ b/packages/core/src/interfaces/index.ts @@ -18,6 +18,7 @@ export * from './event.interface' export * from './event-factory.interface' export * from './event-handler.interface' export * from './event-raw.interface' +export * from './master.interface' export * from './notification.interface' export * from './paginate.dto' export * from './search.dto' diff --git a/packages/core/src/interfaces/master.interface.ts b/packages/core/src/interfaces/master.interface.ts new file mode 100644 index 0000000..964534c --- /dev/null +++ b/packages/core/src/interfaces/master.interface.ts @@ -0,0 +1,10 @@ +import { DetailKey } from './detail-key.interface' + +export interface IMasterDataProvider { + /** + * Get the data for a specific key. + * @param key - The key to identify the data. + * @returns A promise that resolves to the data. + */ + getData(key: DetailKey): Promise +} diff --git a/packages/sequence/package.json b/packages/sequence/package.json index 1a8a593..d32e1dd 100644 --- a/packages/sequence/package.json +++ b/packages/sequence/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/sequence", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "description": "Generate increment sequence with time-rotation", "keywords": [ "mbc", @@ -41,6 +41,6 @@ "access": "public" }, "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.33-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.35-beta.0" } } diff --git a/packages/sequence/src/constants/sequence.constant.ts b/packages/sequence/src/constants/sequence.constant.ts new file mode 100644 index 0000000..0cd24d0 --- /dev/null +++ b/packages/sequence/src/constants/sequence.constant.ts @@ -0,0 +1,13 @@ +type MasterDataType = { + typeCode: string + format: string + startMonth?: number + registerDate?: Date +} + +export const DEFAULT_MASTER_DATA = Symbol('DEFAULT_MASTER_DATA') + +export const DEFAULT_VALUE_MASTER_DATA: MasterDataType = { + typeCode: 'sequence', + format: '%%no%%', +} diff --git a/packages/sequence/src/dto/gen-sequence.dto.ts b/packages/sequence/src/dto/gen-sequence.dto.ts index e092534..becc4a7 100644 --- a/packages/sequence/src/dto/gen-sequence.dto.ts +++ b/packages/sequence/src/dto/gen-sequence.dto.ts @@ -1,11 +1,6 @@ import { ApiProperty } from '@nestjs/swagger' -import { - IsEnum, - IsNumber, - IsObject, - IsOptional, - IsString, -} from 'class-validator' +import { Type } from 'class-transformer' +import { IsEnum, IsObject, IsOptional, IsString } from 'class-validator' import { RotateByEnum } from '../enums/rotate-by.enum' @@ -16,7 +11,8 @@ export class SequenceParamsDto { code1: string @IsString() - code2: string + @IsOptional() + code2?: string @IsOptional() @IsString() @@ -36,42 +32,79 @@ export class SequenceParamsDto { } export class GenSequenceDto { - @IsString() + /** + * Date for sequence generation (optional). + */ @IsOptional() + @Type(() => Date) + @ApiProperty({ + type: Date, + required: false, + description: 'Date for sequence generation.', + }) date?: Date + /** + * Rotation criteria for the sequence (e.g., yearly, fiscal-yearly). + */ @ApiProperty({ enum: RotateByEnum, example: RotateByEnum.FISCAL_YEARLY }) @IsOptional() @IsEnum(RotateByEnum) rotateBy?: RotateByEnum + /** + * Tenant code for identifying the organization. + */ @IsString() + @ApiProperty({ description: 'Code of the tenant (organization).' }) tenantCode: string - @IsObject() - params: SequenceParamsDto - /** - * Format of no + * Type code for specific sequence classification. */ - @IsString() - @IsOptional() - format?: string + @ApiProperty({ + required: true, + description: 'Type code for specific classification.', + }) + typeCode: string +} +export class GenerateFormattedSequenceDto { /** - * Start month of fiscal year (default: 4) + * Date for sequence generation (optional). */ - - @IsNumber() @IsOptional() - startMonth?: number + @Type(() => Date) + @ApiProperty({ + type: Date, + required: false, + description: 'Date for sequence generation.', + }) + date?: Date /** - * Company registration date + * Rotation criteria for the sequence (e.g., yearly, fiscal-yearly). */ + @ApiProperty({ enum: RotateByEnum, example: RotateByEnum.FISCAL_YEARLY }) + @IsOptional() + @IsEnum(RotateByEnum) + rotateBy?: RotateByEnum + /** + * Tenant code for identifying the organization. + */ @IsString() - @IsOptional() - registerDate?: Date + tenantCode: string + + /** + * Type code for specific sequence classification. + */ + @IsObject() + @ApiProperty({ + type: SequenceParamsDto, + description: + 'Parameters for generating the sequence. code1, code2,code3, code4, code5', + }) + params: SequenceParamsDto } diff --git a/packages/sequence/src/entities/sequence.entity.ts b/packages/sequence/src/entities/sequence.entity.ts new file mode 100644 index 0000000..9e499d4 --- /dev/null +++ b/packages/sequence/src/entities/sequence.entity.ts @@ -0,0 +1,10 @@ +export class SequenceEntity { + id: string + no: number + formattedNo: string + issuedAt: Date + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/packages/sequence/src/interfaces/sequence-service.interface.ts b/packages/sequence/src/interfaces/sequence-service.interface.ts index 42e317a..02d8a60 100644 --- a/packages/sequence/src/interfaces/sequence-service.interface.ts +++ b/packages/sequence/src/interfaces/sequence-service.interface.ts @@ -1,14 +1,42 @@ import { DataEntity, DetailKey, IInvoke } from '@mbc-cqrs-serverless/core' -import { GenSequenceDto } from '../dto/gen-sequence.dto' +import { + GenerateFormattedSequenceDto, + GenSequenceDto, +} from '../dto/gen-sequence.dto' +import { SequenceEntity } from '../entities/sequence.entity' export interface ISequenceService { + /** + * Get the current sequence by a specific key. + * @param key - The key to identify the sequence details. + * @returns A promise that resolves to the current sequence's data entity. + */ getCurrentSequence(key: DetailKey): Promise + /** + * Generate a new sequence based on the provided parameters. + * @param dto - The data transfer object containing generation parameters. + * @param opts - Additional options including invocation context. + * @returns A promise that resolves to the newly generated sequence's data entity. + */ genNewSequence( dto: GenSequenceDto, - opts: { + options: { invokeContext: IInvoke }, ): Promise + + /** + * Generate a new sequence with a specified format. + * @param dto - The data transfer object containing parameters for formatted sequence generation. + * @param opts - Additional options including invocation context. + * @returns A promise that resolves to the newly generated formatted sequence's data entity. + */ + generateSequenceItem( + dto: GenerateFormattedSequenceDto, + options: { + invokeContext: IInvoke + }, + ): Promise } diff --git a/packages/sequence/src/sequence-master-factory.ts b/packages/sequence/src/sequence-master-factory.ts new file mode 100644 index 0000000..2a9893d --- /dev/null +++ b/packages/sequence/src/sequence-master-factory.ts @@ -0,0 +1,32 @@ +import { + DetailKey, + DynamoDbService, + IMasterDataProvider, +} from '@mbc-cqrs-serverless/core' +import { Inject, Injectable, Optional } from '@nestjs/common' + +import { DEFAULT_MASTER_DATA } from './constants/sequence.constant' + +Injectable() +export class SequenceMasterDataProvider implements IMasterDataProvider { + private tableName + constructor( + private readonly dynamoDbService: DynamoDbService, + @Inject(DEFAULT_MASTER_DATA) + @Optional() + private readonly defaultValue: Record, + ) { + this.tableName = dynamoDbService.getTableName('master', 'data') + } + async getData(key: DetailKey): Promise { + try { + const item = await this.dynamoDbService.getItem(this.tableName, key) + if (!item) { + return this.defaultValue + } + return item + } catch (error) { + return this.defaultValue + } + } +} diff --git a/packages/sequence/src/sequence.module.ts b/packages/sequence/src/sequence.module.ts index 317cc87..2aa61bf 100644 --- a/packages/sequence/src/sequence.module.ts +++ b/packages/sequence/src/sequence.module.ts @@ -1,6 +1,11 @@ import { DataStoreModule } from '@mbc-cqrs-serverless/core' import { DynamicModule, Module } from '@nestjs/common' +import { + DEFAULT_MASTER_DATA, + DEFAULT_VALUE_MASTER_DATA, +} from './constants/sequence.constant' +import { SequenceMasterDataProvider } from './sequence-master-factory' import { SequencesController } from './sequences.controller' import { ConfigurableModuleClass, @@ -10,7 +15,14 @@ import { SequencesService } from './sequences.service' @Module({ imports: [DataStoreModule], - providers: [SequencesService], + providers: [ + SequencesService, + SequenceMasterDataProvider, + { + provide: DEFAULT_MASTER_DATA, + useValue: DEFAULT_VALUE_MASTER_DATA, + }, + ], exports: [SequencesService], }) export class SequencesModule extends ConfigurableModuleClass { diff --git a/packages/sequence/src/sequences.service.spec.ts b/packages/sequence/src/sequences.service.spec.ts index ee7db66..33bf49b 100644 --- a/packages/sequence/src/sequences.service.spec.ts +++ b/packages/sequence/src/sequences.service.spec.ts @@ -1,13 +1,45 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SequencesService } from './sequences.service'; -import { DynamoDbService } from '@mbc-cqrs-serverless/core'; +import { DynamoDbService, JwtClaims } from '@mbc-cqrs-serverless/core'; import { Logger } from '@nestjs/common'; import { RotateByEnum } from './enums/rotate-by.enum'; import { FiscalYearOptions } from './interfaces/fiscal-year.interface'; +import { SequenceMasterDataProvider } from './sequence-master-factory'; +import { SequenceEntity } from './entities/sequence.entity'; + + +const optionsMock = { + invokeContext: { + event:{ + requestContext: { + accountId: '1', + http: { + + protocol: 'HTTP/1.1', + sourceIp: '127.0.0.1', + userAgent: 'PostmanRuntime/7.28.4', + }, + requestId:'81bf1821-34b0-4dc5-a2ce-685d37d22f8c', + authorizer: { + jwt: { + claims:{ + "custom:tenant": 'MBC', + "custom:roles": '[{"tenant":"MBC","role":"admin"}]' , + } as JwtClaims + } + } + } + }, + context: { + awsRequestId: '81bf1821-34b0-4dc5-a2ce-685d37d22f8c', + } + }, +} describe('SequencesService', () => { let service: SequencesService; let dynamoDbService: DynamoDbService; + let masterService: SequenceMasterDataProvider; const mockTableName = 'mockTableName'; const tenantCode = 'MBC'; @@ -23,12 +55,19 @@ describe('SequencesService', () => { getTableName: jest.fn().mockReturnValue(mockTableName), }, }, + { + provide: SequenceMasterDataProvider, + useValue: { + getData: jest.fn(), + }, + }, Logger, ], }).compile(); service = module.get(SequencesService); dynamoDbService = module.get(DynamoDbService); + masterService = module.get(SequenceMasterDataProvider); }); it('should be defined', () => { @@ -60,7 +99,8 @@ describe('SequencesService', () => { attributes: { fiscal_year: 71, issued_at: "2024-11-08T13:50:26+07:00", - formatted_no: "00TASK1-71-code3001" + formatted_no: "00TASK1-71-code3001", + no:1 }, pk: "SEQ#MBC", seq: 1, @@ -75,180 +115,963 @@ describe('SequencesService', () => { }); // Case 1: Default start month (April) and reference year 1953 - it('should calculate the fiscal year using default start month (April) and reference year 1953', () => { - const options: FiscalYearOptions = { now: new Date('2024-03-15') }; // Before the fiscal year starts (April 1) - const result = service.getFiscalYear(options); - expect(result).toBe(71); // 2023 fiscal year, since 2024-2023 = 1, starting from 1953 - }); - - // Case 2: Custom start month (e.g., July) - it('should calculate the fiscal year using a custom start month (July)', () => { - const options: FiscalYearOptions = { now: new Date('2024-03-15'), startMonth: 7 }; - const result = service.getFiscalYear(options); - expect(result).toBe(71); // The fiscal year 2023 ends on June 30, 2024 - }); - - // Case 3: Custom register time (e.g., starting from 2020) - it('should calculate the fiscal year using a custom register time (2020)', () => { - const options: FiscalYearOptions = { - now: new Date('2024-03-15'), - registerTime: new Date('2020-05-01'), - }; - const result = service.getFiscalYear(options); - expect(result).toBe(4); - }); - - // Case 4: Custom register time and custom start month (e.g., July) - it('should calculate the fiscal year using a custom register time and start month (July)', () => { - const options: FiscalYearOptions = { - now: new Date('2024-03-15'), - registerTime: new Date('2020-05-01'), - startMonth: 7, - }; - const result = service.getFiscalYear(options); - expect(result).toBe(4); - }); - - // Case 5: `now` exactly matches the start of the fiscal year (April 1) - it('should handle the case where now is exactly the start of the fiscal year', () => { - const options: FiscalYearOptions = { now: new Date('2024-04-01') }; // First day of fiscal year - const result = service.getFiscalYear(options); - expect(result).toBe(72); // 2024 fiscal year (72nd fiscal year since 1953) - }); - - // Case 6: `now` is just before the start of the fiscal year (March 31) - it('should handle the case where now is just before the fiscal year starts', () => { - const options: FiscalYearOptions = { now: new Date('2024-03-31') }; // Day before fiscal year starts - const result = service.getFiscalYear(options); - expect(result).toBe(71); // Fiscal year 2023 (71st fiscal year since 1953) - }); - - // Case 7: `now` is in the next fiscal year, but before the start month - it('should calculate the fiscal year when now is after the fiscal year start, but before the start month', () => { - const options: FiscalYearOptions = { - now: new Date('2024-06-01'), // After fiscal year start (April) but before custom start month (July) - startMonth: 7, - registerTime: new Date('2019-01-01'), - }; - const result = service.getFiscalYear(options); - expect(result).toBe(6); // Fiscal year 2024, counting from 2019 + 1 = 6 - }); - - // Case 8: Handle future registerTime (later than now) - it('should return a negative fiscal year when registerTime is in the future', () => { - const options: FiscalYearOptions = { - now: new Date('2024-11-18'), - registerTime: new Date('2025-05-01'), // Future register date - }; - const result = service.getFiscalYear(options); - expect(result).toBe(0); // 2024 fiscal year is negative compared to future registerTime - }); - - // Case 9: `now` and `registerTime` in the same fiscal year - it('should return fiscal year 1 when now and registerTime fall within the same fiscal year', () => { - const options: FiscalYearOptions = { - now: new Date('2024-10-01'), - registerTime: new Date('2024-05-01'), - }; - const result = service.getFiscalYear(options); - expect(result).toBe(1); // Same fiscal year (2024) as registerTime - }); - - // Case 10: No startMonth or registerTime, using defaults (1953) - it('should return the fiscal year using defaults (starting from 1953)', () => { - const options: FiscalYearOptions = { now: new Date('2024-11-18') }; // No startMonth or registerTime - const result = service.getFiscalYear(options); - expect(result).toBe(72); // 2023 fiscal year, starting from 1953 - }); - - // Case 11: Very early registerTime and no startMonth - it('should return a high fiscal year number when registerTime is very early (e.g., 1900)', () => { - const options: FiscalYearOptions = { - now: new Date('2024-11-18'), - registerTime: new Date('1900-01-01'), // Register date far in the past - }; - const result = service.getFiscalYear(options); - expect(result).toBe(125); - }); - - it('should return fiscal year when rotateBy is FISCAL_YEARLY', () => { - const testDate = new Date('2024-02-15'); // February (before April) - expect(service.getRotateValue(RotateByEnum.FISCAL_YEARLY, testDate)).toBe('2023'); - - const testDate2 = new Date('2024-04-15'); // April (new fiscal year) - expect(service.getRotateValue(RotateByEnum.FISCAL_YEARLY, testDate2)).toBe('2024'); - }); + describe('getFiscalYear', () => { + it('should calculate the fiscal year using default start month (April) and reference year 1953', () => { + const options: FiscalYearOptions = { now: new Date('2024-03-15') }; // Before the fiscal year starts (April 1) + const result = service.getFiscalYear(options); + expect(result).toBe(71); // 2023 fiscal year, since 2024-2023 = 1, starting from 1953 + }); + + // Case 2: Custom start month (e.g., July) + it('should calculate the fiscal year using a custom start month (July)', () => { + const options: FiscalYearOptions = { now: new Date('2024-03-15'), startMonth: 7 }; + const result = service.getFiscalYear(options); + expect(result).toBe(71); // The fiscal year 2023 ends on June 30, 2024 + }); + + // Case 3: Custom register time (e.g., starting from 2020) + it('should calculate the fiscal year using a custom register time (2020)', () => { + const options: FiscalYearOptions = { + now: new Date('2024-03-15'), + registerTime: new Date('2020-05-01'), + }; + const result = service.getFiscalYear(options); + expect(result).toBe(4); + }); + + // Case 4: Custom register time and custom start month (e.g., July) + it('should calculate the fiscal year using a custom register time and start month (July)', () => { + const options: FiscalYearOptions = { + now: new Date('2024-03-15'), + registerTime: new Date('2020-05-01'), + startMonth: 7, + }; + const result = service.getFiscalYear(options); + expect(result).toBe(4); + }); + + // Case 5: `now` exactly matches the start of the fiscal year (April 1) + it('should handle the case where now is exactly the start of the fiscal year', () => { + const options: FiscalYearOptions = { now: new Date('2024-04-01') }; // First day of fiscal year + const result = service.getFiscalYear(options); + expect(result).toBe(72); // 2024 fiscal year (72nd fiscal year since 1953) + }); + + // Case 6: `now` is just before the start of the fiscal year (March 31) + it('should handle the case where now is just before the fiscal year starts', () => { + const options: FiscalYearOptions = { now: new Date('2024-03-31') }; // Day before fiscal year starts + const result = service.getFiscalYear(options); + expect(result).toBe(71); // Fiscal year 2023 (71st fiscal year since 1953) + }); + + // Case 7: `now` is in the next fiscal year, but before the start month + it('should calculate the fiscal year when now is after the fiscal year start, but before the start month', () => { + const options: FiscalYearOptions = { + now: new Date('2024-06-01'), // After fiscal year start (April) but before custom start month (July) + startMonth: 7, + registerTime: new Date('2019-01-01'), + }; + const result = service.getFiscalYear(options); + expect(result).toBe(6); // Fiscal year 2024, counting from 2019 + 1 = 6 + }); + + // Case 8: Handle future registerTime (later than now) + it('should return a negative fiscal year when registerTime is in the future', () => { + const options: FiscalYearOptions = { + now: new Date('2024-11-18'), + registerTime: new Date('2025-05-01'), // Future register date + }; + const result = service.getFiscalYear(options); + expect(result).toBe(0); // 2024 fiscal year is negative compared to future registerTime + }); + + // Case 9: `now` and `registerTime` in the same fiscal year + it('should return fiscal year 1 when now and registerTime fall within the same fiscal year', () => { + const options: FiscalYearOptions = { + now: new Date('2024-10-01'), + registerTime: new Date('2024-05-01'), + }; + const result = service.getFiscalYear(options); + expect(result).toBe(1); // Same fiscal year (2024) as registerTime + }); + + // Case 10: No startMonth or registerTime, using defaults (1953) + it('should return the fiscal year using defaults (starting from 1953)', () => { + const options: FiscalYearOptions = { now: new Date('2024-11-18') }; // No startMonth or registerTime + const result = service.getFiscalYear(options); + expect(result).toBe(72); // 2023 fiscal year, starting from 1953 + }); + + // Case 11: Very early registerTime and no startMonth + it('should return a high fiscal year number when registerTime is very early (e.g., 1900)', () => { + const options: FiscalYearOptions = { + now: new Date('2024-11-18'), + registerTime: new Date('1900-01-01'), // Register date far in the past + }; + const result = service.getFiscalYear(options); + expect(result).toBe(125); + }); + + }) - it('should return year when rotateBy is YEARLY', () => { - const testDate = new Date('2024-06-15'); - expect(service.getRotateValue(RotateByEnum.YEARLY, testDate)).toBe('2024'); - }); + describe('getRotateBy', () => { + it('should return fiscal year when rotateBy is FISCAL_YEARLY', () => { + const testDate = new Date('2024-02-15'); // February (before April) + expect(service.getRotateValue(RotateByEnum.FISCAL_YEARLY, testDate)).toBe('2023'); + + const testDate2 = new Date('2024-04-15'); // April (new fiscal year) + expect(service.getRotateValue(RotateByEnum.FISCAL_YEARLY, testDate2)).toBe('2024'); + }); + + it('should return year when rotateBy is YEARLY', () => { + const testDate = new Date('2024-06-15'); + expect(service.getRotateValue(RotateByEnum.YEARLY, testDate)).toBe('2024'); + }); + + it('should return year and month when rotateBy is MONTHLY', () => { + const testDate = new Date('2024-06-15'); + expect(service.getRotateValue(RotateByEnum.MONTHLY, testDate)).toBe('202406'); + + const testDate2 = new Date('2024-01-15'); + expect(service.getRotateValue(RotateByEnum.MONTHLY, testDate2)).toBe('202401'); + }); + + it('should return year, month, and day when rotateBy is DAILY', () => { + const testDate = new Date('2024-06-15'); + expect(service.getRotateValue(RotateByEnum.DAILY, testDate)).toBe('20240615'); + + const testDate2 = new Date('2024-01-05'); + expect(service.getRotateValue(RotateByEnum.DAILY, testDate2)).toBe('20240105'); + }); + + it('should return RotateByEnum.NONE for undefined or unhandled rotateBy', () => { + expect(service.getRotateValue()).toBe(RotateByEnum.NONE); + expect(service.getRotateValue(undefined, new Date('2024-06-15'))).toBe(RotateByEnum.NONE); + }); + + it('should return true if rotateBy is not provided', () => { + const result = service.isIncrementNo(undefined, 2024, 2024, new Date()); + expect(result).toBe(true); + }); + + it('should return true if rotateBy is FISCAL_YEARLY and fiscal year matches', () => { + const result = service.isIncrementNo(RotateByEnum.FISCAL_YEARLY, 2024, 2024, new Date()); + expect(result).toBe(true); + }); + + it('should return false if rotateBy is FISCAL_YEARLY and fiscal year does not match', () => { + const result = service.isIncrementNo(RotateByEnum.FISCAL_YEARLY, 2024, 2023, new Date()); + expect(result).toBe(false); + }); + it('should return false if rotateBy is MONTHLY and issued year does not match current year', () => { + const issuedAt = new Date('2023-06-15'); + // jest.spyOn(global, 'Date').mockImplementation(() => currentDate); + + const result = service.isIncrementNo(RotateByEnum.MONTHLY, 2024, 2024, issuedAt); + expect(result).toBe(false); + }); + + it('should return false if rotateBy is MONTHLY and issued month does not match current month but matches year', () => { + const issuedAt = new Date('2024-05-01'); + const result = service.isIncrementNo(RotateByEnum.MONTHLY, 2024, 2024, issuedAt); + expect(result).toBe(false); + }); + it('should return false if rotateBy is YEARLY and issued year does not match current year', () => { + const issuedAt = new Date('2023-01-15'); + const result = service.isIncrementNo(RotateByEnum.YEARLY, 2024, 2024, issuedAt); + expect(result).toBe(false); + }); + // it('should return true if rotateBy is YEARLY and issued year matches current year', () => { + // const issuedAt = new Date('2024-01-15'); + // const currentDate = new Date('2024-06-15'); + // jest.spyOn(global, 'Date').mockImplementation(() => currentDate); // Mock current date + + // const result = service.isIncrementNo(RotateByEnum.YEARLY, 2024, 2024, issuedAt); + // expect(result).toBe(true); + // }); + it('should return true if rotateBy is MONTHLY and issued month matches current month and year', () => { + const issuedAt = new Date(); + const result = service.isIncrementNo(RotateByEnum.MONTHLY, 2024, 2024, issuedAt); + expect(result).toBe(true); + }); + }) - it('should return year and month when rotateBy is MONTHLY', () => { - const testDate = new Date('2024-06-15'); - expect(service.getRotateValue(RotateByEnum.MONTHLY, testDate)).toBe('202406'); + describe('generateSequenceItem', () => { + it('should call generateSequenceItem with none rotation', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#none", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:44:15+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "7724a67e-ded6-4ebb-9c88-c14070e24012", + "name": "none", + "sk": "sequence#TODO#none", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-11-27T13:44:16+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#none", + no: 1, + formattedNo:"1", + issuedAt: (new Date("2024-11-27T13:44:16+07:00")), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date: new Date("2024-11-27T13:44:16+07:00"), + rotateBy: RotateByEnum.NONE, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); + }) + it('should call generateSequenceItem with none rotation at second time', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#none", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:44:15+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "7724a67e-ded6-4ebb-9c88-c14070e24012", + "name": "none", + "sk": "sequence#TODO#none", + "pk": "SEQ#MBC", + "seq": 2, + "updatedAt": "2024-11-27T13:44:16+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#none", + no: 2, + formattedNo:"2", + issuedAt: new Date("2024-11-27T13:44:16+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + date: new Date("2024-11-27T13:44:16+07:00"), + params: { + code1: 'TODO', + }, + rotateBy: RotateByEnum.NONE, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); + }) + it ('should call generateSequenceItem with fiscal yearly rotation', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#2024", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:54:04+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "f4c867f7-0b25-439e-896a-d1df1537d589", + "name": "fiscal_yearly", + "sk": "sequence#TODO#2024", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-11-27T13:54:04+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#2024", + no: 1, + formattedNo:"1", + issuedAt: new Date("2024-11-27T13:54:04+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date: new Date("2024-11-27T13:54:04+07:00"), + rotateBy: RotateByEnum.FISCAL_YEARLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - const testDate2 = new Date('2024-01-15'); - expect(service.getRotateValue(RotateByEnum.MONTHLY, testDate2)).toBe('202401'); - }); + }) + it ('should call generateSequenceItem with fiscal yearly rotation, argument is a fiscal year that is the same as the previous fiscal year', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#2024", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:54:04+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "f4c867f7-0b25-439e-896a-d1df1537d589", + "name": "fiscal_yearly", + "sk": "sequence#TODO#2024", + "pk": "SEQ#MBC", + "seq": 2, + "updatedAt": "2024-11-27T13:54:04+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#2024", + no: 2, + formattedNo:"2", + issuedAt: new Date("2024-11-27T13:54:04+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date: new Date("2024-11-27T13:54:04+07:00"), + rotateBy: RotateByEnum.FISCAL_YEARLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - it('should return year, month, and day when rotateBy is DAILY', () => { - const testDate = new Date('2024-06-15'); - expect(service.getRotateValue(RotateByEnum.DAILY, testDate)).toBe('20240615'); + }) + it ('should call generateSequenceItem with fiscal yearly rotation, argument is a fiscal year other than the previous fiscal year', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#2024", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-03-27T13:54:04+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "f4c867f7-0b25-439e-896a-d1df1537d589", + "name": "fiscal_yearly", + "sk": "sequence#TODO#2024", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-03-27T13:54:04+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#2024", + no: 1, + formattedNo:"1", + issuedAt: new Date("2024-03-27T13:54:04+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date : new Date("2024-03-27T13:54:04+07:00"), + rotateBy: RotateByEnum.FISCAL_YEARLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - const testDate2 = new Date('2024-01-05'); - expect(service.getRotateValue(RotateByEnum.DAILY, testDate2)).toBe('20240105'); - }); + }) + it ('should call generateSequenceItem with yearly rotation', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#2024", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:54:04+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "f4c867f7-0b25-439e-896a-d1df1537d589", + "name": "yearly", + "sk": "sequence#TODO#2024", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-11-27T13:54:04+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#2024", + no: 1, + formattedNo:"1", + issuedAt: new Date("2024-11-27T13:54:04+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date : new Date("2024-11-27T13:54:04+07:00"), + rotateBy: RotateByEnum.YEARLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - it('should return RotateByEnum.NONE for undefined or unhandled rotateBy', () => { - expect(service.getRotateValue()).toBe(RotateByEnum.NONE); - expect(service.getRotateValue(undefined, new Date('2024-06-15'))).toBe(RotateByEnum.NONE); - }); + }) + it ('should call generateSequenceItem with yearly rotation, argument is a year that is the same as the previous year', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#2024", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:54:04+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "f4c867f7-0b25-439e-896a-d1df1537d589", + "name": "yearly", + "sk": "sequence#TODO#2024", + "pk": "SEQ#MBC", + "seq": 2, + "updatedAt": "2024-11-27T13:54:04+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#2024", + no: 2, + formattedNo:"2", + issuedAt: new Date("2024-11-27T13:54:04+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + date : new Date("2024-11-27T13:54:04+07:00"), + params: { + code1: 'TODO', + }, + rotateBy: RotateByEnum.YEARLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - it('should return true if rotateBy is not provided', () => { - const result = service.isIncrementNo(undefined, 2024, 2024, new Date()); - expect(result).toBe(true); - }); + }) + it ('should call generateSequenceItem with yearly rotation, argument is a year other than the previous year', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#2025", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2025-03-27T13:54:04+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "f4c867f7-0b25-439e-896a-d1df1537d589", + "name": "yearly", + "sk": "sequence#TODO#2025", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2025-03-27T13:54:04+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#2025", + no: 1, + formattedNo:"1", + issuedAt: new Date("2025-03-27T13:54:04+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date : new Date("2025-03-27T13:54:04+07:00"), + rotateBy: RotateByEnum.YEARLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - it('should return true if rotateBy is FISCAL_YEARLY and fiscal year matches', () => { - const result = service.isIncrementNo(RotateByEnum.FISCAL_YEARLY, 2024, 2024, new Date()); - expect(result).toBe(true); - }); + }) + it ('should call generateSequenceItem with monthly rotation', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#202411", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:56:39+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "bd482504-6076-46a3-8503-48063f4debd8", + "name": "monthly", + "sk": "sequence#TODO#202411", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-11-27T13:56:39+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#202411", + no: 1, + formattedNo:"1", + issuedAt: new Date("2024-11-27T13:56:39+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + date : new Date("2024-11-27T13:56:39+07:00"), + params: { + code1: 'TODO', + }, + rotateBy: RotateByEnum.MONTHLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - it('should return false if rotateBy is FISCAL_YEARLY and fiscal year does not match', () => { - const result = service.isIncrementNo(RotateByEnum.FISCAL_YEARLY, 2024, 2023, new Date()); - expect(result).toBe(false); - }); - it('should return false if rotateBy is MONTHLY and issued year does not match current year', () => { - const issuedAt = new Date('2023-06-15'); - // jest.spyOn(global, 'Date').mockImplementation(() => currentDate); + }) + it ('should call generateSequenceItem with monthly rotation, argument is a month other than the previous month', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#202411", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:56:39+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "bd482504-6076-46a3-8503-48063f4debd8", + "name": "monthly", + "sk": "sequence#TODO#202411", + "pk": "SEQ#MBC", + "seq": 2, + "updatedAt": "2024-11-27T13:56:39+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#202411", + no: 2, + formattedNo:"2", + issuedAt: new Date("2024-11-27T13:56:39+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date : new Date("2024-11-27T13:56:39+07:00"), + rotateBy: RotateByEnum.MONTHLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - const result = service.isIncrementNo(RotateByEnum.MONTHLY, 2024, 2024, issuedAt); - expect(result).toBe(false); - }); + }) + it ('should call generateSequenceItem with monthly rotation, argument is a month that is the same as the previous month', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#202412", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-12-27T13:56:39+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "bd482504-6076-46a3-8503-48063f4debd8", + "name": "monthly", + "sk": "sequence#TODO#202412", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-12-27T13:56:39+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#202412", + no: 1, + formattedNo:"1", + issuedAt: new Date("2024-12-27T13:56:39+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date : new Date("2024-12-27T13:56:39+07:00"), + rotateBy: RotateByEnum.MONTHLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - it('should return false if rotateBy is MONTHLY and issued month does not match current month but matches year', () => { - const issuedAt = new Date('2024-05-01'); - const result = service.isIncrementNo(RotateByEnum.MONTHLY, 2024, 2024, issuedAt); - expect(result).toBe(false); - }); - it('should return false if rotateBy is YEARLY and issued year does not match current year', () => { - const issuedAt = new Date('2023-01-15'); - const result = service.isIncrementNo(RotateByEnum.YEARLY, 2024, 2024, issuedAt); - expect(result).toBe(false); - }); - it('should return true if rotateBy is YEARLY and issued year matches current year', () => { - const issuedAt = new Date('2024-01-15'); - const currentDate = new Date('2024-06-15'); - jest.spyOn(global, 'Date').mockImplementation(() => currentDate); // Mock current date + }) + it ('should call generateSequenceItem with daily rotation ', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#20241127", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:56:39+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "bd482504-6076-46a3-8503-48063f4debd8", + "name": "daily", + "sk": "sequence#TODO#20241127", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-11-27T13:56:39+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#20241127", + no: 1, + formattedNo:"1", + issuedAt: new Date("2024-11-27T13:56:39+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date : new Date("2024-11-27T13:56:39+07:00"), + rotateBy: RotateByEnum.DAILY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); + }) + it ('should call generateSequenceItem with daily rotation, argument is a day other than the previous day', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#20241127", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T13:56:39+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "bd482504-6076-46a3-8503-48063f4debd8", + "name": "daily", + "sk": "sequence#TODO#20241127", + "pk": "SEQ#MBC", + "seq": 2, + "updatedAt": "2024-11-27T13:56:39+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#20241127", + no: 2, + formattedNo:"2", + issuedAt: new Date("2024-11-27T13:56:39+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + date : new Date("2024-11-27T13:56:39+07:00"), + params: { + code1: 'TODO', + }, + rotateBy: RotateByEnum.DAILY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); + }) + it ('should call generateSequenceItem with daily rotation, the argument is a day that is the same as the previous day', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#20241227", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-12-27T13:56:39+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "bd482504-6076-46a3-8503-48063f4debd8", + "name": "daily", + "sk": "sequence#TODO#20241227", + "pk": "SEQ#MBC", + "seq": 2, + "updatedAt": "2024-12-27T13:56:39+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#20241227", + no: 2, + formattedNo:"2", + issuedAt: new Date("2024-12-27T13:56:39+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date : new Date("2024-12-27T13:56:39+07:00"), + rotateBy: RotateByEnum.DAILY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); - const result = service.isIncrementNo(RotateByEnum.YEARLY, 2024, 2024, issuedAt); - expect(result).toBe(true); - }); - it('should return true if rotateBy is MONTHLY and issued month matches current month and year', () => { - const issuedAt = new Date(); - const result = service.isIncrementNo(RotateByEnum.MONTHLY, 2024, 2024, issuedAt); - expect(result).toBe(true); + }) + it('should call generateSequenceItem with none rotation, the arguments have code 1, code 2, code 3, code4, code5 ', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%code1%%-%%code2%%-%%code3%%-%%code4%%-%%code5%%-%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#ID2#ID3#ID4#ID5#none", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T14:36:18+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "0ff67305-18b6-4a9e-9226-b4f56fec6592", + "name": "none", + "sk": "sequence#TODO#ID2#ID3#ID4#ID5#none", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-11-27T14:36:18+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#ID2#ID3#ID4#ID5#none", + no: 1, + formattedNo:"TODO-ID2-ID3-ID4-ID5-1", + issuedAt: new Date("2024-11-27T13:44:16+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + rotateBy: RotateByEnum.NONE, + date: new Date("2024-11-27T13:44:16+07:00"), + params: { + code1: 'TODO', + code2: 'ID2', + code3: 'ID3', + code4: 'ID4', + code5: 'ID5', + }, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); + }) + it('should call generateSequenceItem with none rotation, format is %%code1%%-%%fiscal_year%%-%%no%%', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%code1%%-%%fiscal_year%%-%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#ID2#ID3#ID4#ID5#none", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-27T14:36:18+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "0ff67305-18b6-4a9e-9226-b4f56fec6592", + "name": "none", + "sk": "sequence#TODO#ID2#ID3#ID4#ID5#none", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-11-27T14:36:18+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#ID2#ID3#ID4#ID5#none", + no: 1, + formattedNo:"TODO-72-1", + issuedAt: new Date("2024-11-27T13:44:16+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + date: new Date("2024-11-27T13:44:16+07:00"), + rotateBy: RotateByEnum.NONE, + params: { + code1: 'TODO', + code2: 'ID2', + code3: 'ID3', + code4: 'ID4', + code5: 'ID5', + }, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); + }) + it('should call generateSequenceItem with none rotation, format is %%code1%%-%%month%%-%%no%%', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%code1%%-%%month%%-%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#none", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-06-13T13:44:16+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "0ff67305-18b6-4a9e-9226-b4f56fec6592", + "name": "none", + "sk": "sequence#TODO#none", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-06-13T13:44:16+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#none", + no: 1, + formattedNo:"TODO-6-1", + issuedAt: new Date("2024-06-13T13:44:16+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + rotateBy: RotateByEnum.YEARLY, + date: new Date("2024-06-13T13:44:16+07:00"), + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); + }) + it('should call generateSequenceItem with none rotation, format is %%code1%%-%%day%%-%%no%%', async () => { + const mockMasterData = { + typeCode: 'sequence', + format: '%%code1%%-%%day%%-%%no%%', + } + const mockUpdate ={ + "code": "sequence#TODO#none", + "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "createdIp": "127.0.0.1", + "tenantCode": "MBC", + "type": "sequence", + "createdAt": "2024-11-15T14:36:18+07:00", + "updatedIp": "127.0.0.1", + "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4", + "requestId": "0ff67305-18b6-4a9e-9226-b4f56fec6592", + "name": "none", + "sk": "sequence#TODO#none", + "pk": "SEQ#MBC", + "seq": 1, + "updatedAt": "2024-11-15T14:36:18+07:00" + } + jest.spyOn(masterService, 'getData').mockResolvedValue(mockMasterData); + jest.spyOn(dynamoDbService, 'updateItem').mockResolvedValue(mockUpdate); + const mockSequenceResponse = new SequenceEntity({ + id: "SEQ#MBC#sequence#TODO#none", + no: 1, + formattedNo:"TODO-15-1", + issuedAt: new Date("2024-11-15T13:44:16+07:00"), + }) + const result = await service.generateSequenceItem( + { + tenantCode: tenantCode, + params: { + code1: 'TODO', + }, + date: new Date("2024-11-15T13:44:16+07:00"), + rotateBy: RotateByEnum.YEARLY, + }, + optionsMock + ); + expect(result).toEqual(mockSequenceResponse); + }) }); }); diff --git a/packages/sequence/src/sequences.service.ts b/packages/sequence/src/sequences.service.ts index 4a4992e..a646e75 100644 --- a/packages/sequence/src/sequences.service.ts +++ b/packages/sequence/src/sequences.service.ts @@ -2,33 +2,49 @@ import { DataEntity, DetailKey, DynamoDbService, + generateId, getUserContext, IInvoke, KEY_SEPARATOR, + masterPk, + seqPk, toISOStringWithTimezone, } from '@mbc-cqrs-serverless/core' import { Injectable, Logger } from '@nestjs/common' -import { GenSequenceDto, SequenceParamsDto } from './dto/gen-sequence.dto' +import { + GenerateFormattedSequenceDto, + GenSequenceDto, + SequenceParamsDto, +} from './dto/gen-sequence.dto' +import { SequenceEntity } from './entities/sequence.entity' import { RotateByEnum } from './enums/rotate-by.enum' import { FiscalYearOptions } from './interfaces/fiscal-year.interface' import { ISequenceService } from './interfaces/sequence-service.interface' +import { SequenceMasterDataProvider } from './sequence-master-factory' @Injectable() export class SequencesService implements ISequenceService { private readonly logger = new Logger(SequencesService.name) private readonly tableName: string - constructor(private readonly dynamoDbService: DynamoDbService) { + constructor( + private readonly dynamoDbService: DynamoDbService, + private readonly masterDataProvider: SequenceMasterDataProvider, + ) { this.tableName = dynamoDbService.getTableName('sequences') this.logger.debug('tableName: ' + this.tableName) } + /** + * @deprecated This method is deprecated at V0.2. + */ async getCurrentSequence(key: DetailKey): Promise { return await this.dynamoDbService.getItem(this.tableName, key) } /** + * @deprecated This method is deprecated at V0.2. * Seq data structure * - pk: SEQ#tenantCode * - sk: typeCode#rotateValue ( e.x: `user#20230401` ) @@ -37,37 +53,91 @@ export class SequencesService implements ISequenceService { * - tenant_code: tenantCode * - type: typeCode * - seq: sequence value ( atomic counter ) + */ + async genNewSequence( + dto: GenSequenceDto, + options: { + invokeContext: IInvoke + }, + ): Promise { + const rotateVal = this.getRotateValue(dto.rotateBy, dto.date) + const pk = `SEQ${KEY_SEPARATOR}${dto.tenantCode}` + const sk = `${dto.typeCode}${KEY_SEPARATOR}${rotateVal}` + + const sourceIp = + options.invokeContext?.event?.requestContext?.http?.sourceIp + const userContext = getUserContext(options.invokeContext) + const userId = userContext.userId || 'system' + const now = new Date() + const item = await this.dynamoDbService.updateItem( + this.tableName, + { pk, sk }, + { + set: { + code: sk, + name: dto.rotateBy || 'none', + tenantCode: dto.tenantCode, + type: dto.typeCode, + seq: { ifNotExists: 0, incrementBy: 1 }, + requestId: options.invokeContext?.context?.awsRequestId, + createdAt: { ifNotExists: now }, + createdBy: { ifNotExists: userId }, + createdIp: { ifNotExists: sourceIp }, + updatedAt: now, + updatedBy: userId, + updatedIp: sourceIp, + }, + }, + ) + + return item + } + + /** + * Seq data structure + * - pk: SEQ#tenantCode + * - sk: typeCode#code1#code2#code3#code4#code5rotateValue ( e.x: `user#20230401` ) + * - code: typeCode#rotateValue + * - name: rotateBy ( e.x: `daily` ) + * - tenant_code: tenantCode + * - type: typeCode + * - seq: sequence value ( atomic counter ) * - requestId: requestId * - createdAt: createdAt * - createdBy: createdBy * - createdIp: createdIp * - attributes: { - * formatted_no: formattedNo ( ) + * formatted_no: formattedNo ( e.x: `2023-04-01-0001` ) * fiscal_year: fiscalYear * issued_at: issuedAt * } */ - async genNewSequence( - dto: GenSequenceDto, - opts: { invokeContext: IInvoke }, - ): Promise { - const { - date, - rotateBy, - format = '%%no%%', - tenantCode, - params, - registerDate, - startMonth, - } = dto - const pk = `SEQ${KEY_SEPARATOR}${tenantCode}` + async generateSequenceItem( + dto: GenerateFormattedSequenceDto, + options: { invokeContext: IInvoke }, + ): Promise { + const { date, rotateBy, tenantCode, params } = dto + + const generalMasterPk = masterPk(tenantCode) + const generalMasterSk = `SEQ${KEY_SEPARATOR}${params?.code1}` + this.logger.log('general master pk: ', generalMasterPk) + this.logger.log('general master sk: ', generalMasterSk) + const masterData = await this.masterDataProvider.getData({ + pk: generalMasterPk, + sk: generalMasterSk, + }) + // Get master data for the tenant + const { format, typeCode, registerDate, startMonth } = masterData + const pk = seqPk(tenantCode) + // Construct the sort key for the sequence let sk = [ - params.code1, - params.code2, - params.code3, - params.code4, - params.code5, + typeCode, + params?.code1, + params?.code2, + params?.code3, + params?.code4, + params?.code5, ] .filter(Boolean) .join(KEY_SEPARATOR) @@ -79,91 +149,48 @@ export class SequencesService implements ISequenceService { registerTime: registerDate, startMonth, }) - const sourceIp = opts.invokeContext?.event?.requestContext?.http?.sourceIp - const userContext = getUserContext(opts.invokeContext) + const sourceIp = + options.invokeContext?.event?.requestContext?.http?.sourceIp + const userContext = getUserContext(options.invokeContext) const userId = userContext.userId || 'system' - // Helper function for building the update data - const buildUpdateData = (seq: number, formattedNo: string, sk: string) => ({ - set: { - code: sk, - name: rotateBy || 'none', - tenantCode, - type: params.code1, - seq, - requestId: opts.invokeContext?.context?.awsRequestId, - createdAt: { ifNotExists: now }, - createdBy: { ifNotExists: userId }, - createdIp: { ifNotExists: sourceIp }, - attributes: { - formatted_no: formattedNo, - fiscal_year: nowFiscalYear, - issued_at: issuedAt, + const rotateVal = this.getRotateValue(rotateBy, date) + sk = `${sk}${KEY_SEPARATOR}${rotateVal}` + + const item = await this.dynamoDbService.updateItem( + this.tableName, + { pk, sk }, + { + set: { + code: sk, + name: dto.rotateBy || 'none', + tenantCode: dto.tenantCode, + type: typeCode, + seq: { ifNotExists: 0, incrementBy: 1 }, + requestId: options.invokeContext?.context?.awsRequestId, + createdAt: { ifNotExists: now }, + createdBy: { ifNotExists: userId }, + createdIp: { ifNotExists: sourceIp }, + updatedAt: now, + updatedBy: userId, + updatedIp: sourceIp, }, - updatedAt: now, - updatedBy: userId, - updatedIp: sourceIp, }, - }) - - const sequenceData = await this.dynamoDbService.getItem(this.tableName, { - pk, - sk, - }) - let fixNo = 0 - let rotateSequenceData = null - - // Handle rotating sequence data if date is provided - if (date) { - const rotateSortKeyVal = this.getRotateValue(rotateBy, date) - rotateSequenceData = await this.dynamoDbService.getItem(this.tableName, { - pk, - sk: `${sk}${KEY_SEPARATOR}${rotateSortKeyVal}`, - }) - fixNo = rotateSequenceData ? rotateSequenceData.seq + 1 : 1 - } else if (!date) { - if (sequenceData) { - fixNo = this.isIncrementNo( - rotateBy, - nowFiscalYear, - sequenceData.attributes.fiscal_year, - new Date(sequenceData.issuedAt), - ) - ? sequenceData.seq + 1 - : 1 - } else { - fixNo = 1 - } - } + ) const formatDict = this.createFormatDict( - params, nowFiscalYear, - fixNo, + item.seq, date || now, + { ...params }, ) const formattedNo = this.createFormattedNo(format, formatDict) - - // Common update operation - if (rotateSequenceData) { - return await this.dynamoDbService.updateItem( - this.tableName, - { pk: rotateSequenceData.pk, sk: rotateSequenceData.sk }, - buildUpdateData(fixNo, formattedNo, sk), - ) - } - - // Non-rotate logic - if (date) { - const rotateSortKeyVal = this.getRotateValue(rotateBy, date) - sk = `${sk}${KEY_SEPARATOR}${rotateSortKeyVal}` - } - - return await this.dynamoDbService.updateItem( - this.tableName, - { pk, sk }, - buildUpdateData(fixNo, formattedNo, sk), - ) + return new SequenceEntity({ + id: generateId(item.pk, item.sk), + no: item.seq, + formattedNo: formattedNo, + issuedAt: new Date(issuedAt), + }) } getRotateValue(rotateBy?: RotateByEnum, forDate?: Date) { @@ -273,10 +300,10 @@ export class SequencesService implements ISequenceService { } createFormatDict( - sequenceParams: SequenceParamsDto, fiscalYear: number, fixNo: number, now: Date, + sequenceParams?: SequenceParamsDto, ) { return { ...sequenceParams, diff --git a/packages/task/package.json b/packages/task/package.json index 178f7c3..cebfa93 100644 --- a/packages/task/package.json +++ b/packages/task/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/task", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "description": "long-running task", "keywords": [ "mbc", @@ -41,6 +41,6 @@ "access": "public" }, "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.33-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.35-beta.0" } } diff --git a/packages/ui-setting/package.json b/packages/ui-setting/package.json index f5d816b..49d11eb 100644 --- a/packages/ui-setting/package.json +++ b/packages/ui-setting/package.json @@ -1,6 +1,6 @@ { "name": "@mbc-cqrs-serverless/ui-setting", - "version": "0.1.33-beta.0", + "version": "0.1.35-beta.0", "description": "Setting master data", "keywords": [ "mbc", @@ -41,6 +41,6 @@ "access": "public" }, "dependencies": { - "@mbc-cqrs-serverless/core": "^0.1.33-beta.0" + "@mbc-cqrs-serverless/core": "^0.1.35-beta.0" } } diff --git a/tsconfig.build.json b/tsconfig.build.json index 8d35462..6f66f70 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,4 +3,4 @@ "removeComments": false, }, "extends": "./tsconfig.json" -} \ No newline at end of file +}