diff --git a/.cursor/rules/read-write-controllers.mdc b/.cursor/rules/read-write-controllers.mdc index c718cf1..0c551d2 100644 --- a/.cursor/rules/read-write-controllers.mdc +++ b/.cursor/rules/read-write-controllers.mdc @@ -38,6 +38,7 @@ List requirements: - Shared pagination query (`page`/`limit` or `offset`/`limit`) via `PaginationQueryDto` - Handler **must** use `@Pagination()` and return `{ data, total }` — never build `meta` here (see `.cursor/rules/pagination-response.mdc`) - Service `visibleFields` whitelist: default **all non-secret** attributes; modules may narrow. Project in the **service**, not the controller +- FK relations in list/detail (and write responses that reuse the mapper) MUST be nested objects via `pickRelation` — see `.cursor/rules/relation-response.mdc` - List query must be extendable (e.g. `extendListQuery(qb, filters)` on the repository/service) so joins/extra predicates can be added without forking list ## Write controller diff --git a/.cursor/rules/relation-response.mdc b/.cursor/rules/relation-response.mdc new file mode 100644 index 0000000..cf56cd0 --- /dev/null +++ b/.cursor/rules/relation-response.mdc @@ -0,0 +1,33 @@ +--- +description: List/detail (and write responses that reuse the mapper) embed FK relations as objects via pickRelation +globs: "src/modules/**/*.ts,src/common/http/response/**/*.ts" +alwaysApply: false +--- + +# Relation Response Objects + +List, detail, and write handlers that reuse the same mapper MUST embed foreign keys as nested objects, not bare ids. + +## Field lists + +- Default catalog fields: `DEFAULT_RELATION_FIELDS` (`id`, `code`, `name`) from `src/common/http/response/` +- Override per entity with a module/local constant (users: `USER_RELATION_FIELDS` = `id`, `username`) +- Use `pickRelation(source, fields)` only — do not hand-roll partial copies + +## Mapping + +- Request DTOs still accept `*Id` (`divisionId`); the response key is the relation name (`division`, not `divisionId`) +- Null FK → `null` (not omitted) +- Never expose secrets (`passwordHash`, tokens) in relation objects +- Load relations in the repository (joins or batch-load); map with `pickRelation` in the service + +```typescript +// BAD +return { divisionId: branch.divisionId, createdBy: branch.createdBy } + +// GOOD +return { + division: pickRelation(branch.division, DEFAULT_RELATION_FIELDS), + createdBy: pickRelation(branch.createdByUser, USER_RELATION_FIELDS), +} +``` diff --git a/src/common/http/response/index.ts b/src/common/http/response/index.ts index c524ffc..d771ae6 100644 --- a/src/common/http/response/index.ts +++ b/src/common/http/response/index.ts @@ -22,3 +22,13 @@ export { PAGINATION_DEFAULT_LIMIT, PAGINATION_MAX_LIMIT, } from './pagination.constants'; +export { + DEFAULT_RELATION_FIELDS, + pickDefaultRelation, + pickRelation, + pickUserRelation, + USER_RELATION_FIELDS, + type DefaultRelation, + type UserRelation, +} from './relation-fields'; +export { DefaultRelationDto, UserRelationDto } from './relation.dto'; diff --git a/src/common/http/response/relation-fields.spec.ts b/src/common/http/response/relation-fields.spec.ts new file mode 100644 index 0000000..7239808 --- /dev/null +++ b/src/common/http/response/relation-fields.spec.ts @@ -0,0 +1,78 @@ +import { + DEFAULT_RELATION_FIELDS, + pickDefaultRelation, + pickRelation, + pickUserRelation, + USER_RELATION_FIELDS, +} from './relation-fields'; + +describe('pickRelation', () => { + const catalog = { + id: 'div-1', + code: 'JKT', + name: 'Jakarta', + status: 'active', + extra: 'secret', + }; + + const user = { + id: 'user-1', + username: 'admin', + passwordHash: 'hashed', + }; + + it('picks default id, code, name fields', () => { + expect(pickRelation(catalog, DEFAULT_RELATION_FIELDS)).toEqual({ + id: 'div-1', + code: 'JKT', + name: 'Jakarta', + }); + }); + + it('picks a custom field list for users', () => { + expect(pickRelation(user, USER_RELATION_FIELDS)).toEqual({ + id: 'user-1', + username: 'admin', + }); + }); + + it('returns null for null or undefined sources', () => { + const missing: typeof catalog | null = null; + const unset: typeof catalog | undefined = undefined; + expect( + pickRelation( + missing, + DEFAULT_RELATION_FIELDS, + ), + ).toBeNull(); + expect( + pickRelation( + unset, + DEFAULT_RELATION_FIELDS, + ), + ).toBeNull(); + }); + + it('does not copy fields outside the list', () => { + const picked = pickRelation(catalog, DEFAULT_RELATION_FIELDS); + expect(picked).not.toHaveProperty('status'); + expect(picked).not.toHaveProperty('extra'); + }); +}); + +describe('pickDefaultRelation / pickUserRelation', () => { + it('maps catalog and user sources without leaking extra fields', () => { + expect( + pickDefaultRelation({ + id: 'div-1', + code: 'JKT', + name: 'Jakarta', + }), + ).toEqual({ id: 'div-1', code: 'JKT', name: 'Jakarta' }); + expect(pickDefaultRelation(null)).toBeNull(); + expect(pickUserRelation({ id: 'user-1', username: 'admin' })).toEqual({ + id: 'user-1', + username: 'admin', + }); + }); +}); diff --git a/src/common/http/response/relation-fields.ts b/src/common/http/response/relation-fields.ts new file mode 100644 index 0000000..d7c1c1b --- /dev/null +++ b/src/common/http/response/relation-fields.ts @@ -0,0 +1,42 @@ +export const DEFAULT_RELATION_FIELDS = ['id', 'code', 'name'] as const; +export const USER_RELATION_FIELDS = ['id', 'username'] as const; + +export type DefaultRelation = { + readonly id: string; + readonly code: string; + readonly name: string; +}; + +export type UserRelation = { + readonly id: string; + readonly username: string; +}; + +export function pickRelation>( + source: T | null | undefined, + fields: readonly K[], +): Pick, K> | null { + if (source == null) { + return null; + } + const result = {} as Pick, K>; + for (const field of fields) { + result[field] = source[field]; + } + return result; +} + +export function pickDefaultRelation( + source: DefaultRelation | null | undefined, +): DefaultRelation | null { + return pickRelation(source, DEFAULT_RELATION_FIELDS); +} + +export function pickUserRelation(source: UserRelation): UserRelation { + return ( + pickRelation(source, USER_RELATION_FIELDS) ?? { + id: source.id, + username: source.username, + } + ); +} diff --git a/src/common/http/response/relation.dto.ts b/src/common/http/response/relation.dto.ts new file mode 100644 index 0000000..9575aa5 --- /dev/null +++ b/src/common/http/response/relation.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DefaultRelationDto { + @ApiProperty({ format: 'uuid' }) + id!: string; + + @ApiProperty() + code!: string; + + @ApiProperty() + name!: string; +} + +export class UserRelationDto { + @ApiProperty({ format: 'uuid' }) + id!: string; + + @ApiProperty() + username!: string; +} diff --git a/src/modules/configuration/branches/branch.ts b/src/modules/configuration/branches/branch.ts index 7faf99e..1df2741 100644 --- a/src/modules/configuration/branches/branch.ts +++ b/src/modules/configuration/branches/branch.ts @@ -21,6 +21,19 @@ export type Branch = { readonly updatedAt: DateTime; readonly createdBy: string; readonly updatedBy: string; + readonly division: { + readonly id: string; + readonly code: string; + readonly name: string; + } | null; + readonly createdByUser: { + readonly id: string; + readonly username: string; + }; + readonly updatedByUser: { + readonly id: string; + readonly username: string; + }; }; export type CreateBranchInput = { diff --git a/src/modules/configuration/branches/branches.repository.spec.ts b/src/modules/configuration/branches/branches.repository.spec.ts index f202673..77b7e65 100644 --- a/src/modules/configuration/branches/branches.repository.spec.ts +++ b/src/modules/configuration/branches/branches.repository.spec.ts @@ -26,6 +26,7 @@ describe('BranchesRepository', () => { const del = jest.fn(); const transaction = jest.fn(); const $dynamic = jest.fn(); + const leftJoin = jest.fn(); const db = { select, @@ -56,6 +57,13 @@ describe('BranchesRepository', () => { updatedBy: 'user-1', }; + const joinedRow = { + branch: row, + division: { id: 'div-1', code: 'JKT', name: 'Jakarta' }, + createdByUser: { id: 'user-1', username: 'admin' }, + updatedByUser: { id: 'user-1', username: 'admin' }, + }; + const createInput = { code: 'JKT_01', name: 'Jakarta Pusat', @@ -68,16 +76,34 @@ describe('BranchesRepository', () => { userId: 'user-1', }; - beforeEach(async () => { - jest.clearAllMocks(); - where.mockImplementation(() => ({ limit, orderBy })); - orderBy.mockImplementation(() => ({ limit })); - limit.mockImplementation(() => ({ offset })); - offset.mockResolvedValue([row]); - from.mockImplementation(() => ({ + const joinChain = () => { + const chain: { + leftJoin: jest.Mock; + where: typeof where; + $dynamic: typeof $dynamic; + } = { + leftJoin: jest.fn(), where, $dynamic, + }; + chain.leftJoin.mockReturnValue(chain); + return chain; + }; + + beforeEach(async () => { + jest.clearAllMocks(); + where.mockImplementation(() => ({ limit, orderBy, returning })); + orderBy.mockImplementation(() => ({ limit })); + limit.mockImplementation(() => ({ + offset, + then: ( + resolve: (value: (typeof joinedRow)[]) => unknown, + reject?: (reason: unknown) => unknown, + ) => Promise.resolve([joinedRow]).then(resolve, reject), })); + offset.mockResolvedValue([joinedRow]); + from.mockImplementation(() => joinChain()); + leftJoin.mockImplementation(() => joinChain()); $dynamic.mockReturnValue({ where }); select.mockImplementation(() => ({ from })); values.mockReturnValue({ returning }); @@ -86,7 +112,6 @@ describe('BranchesRepository', () => { update.mockReturnValue({ set }); del.mockReturnValue({ where }); returning.mockResolvedValue([row]); - where.mockImplementation(() => ({ limit, orderBy, returning })); const moduleRef: TestingModule = await Test.createTestingModule({ providers: [BranchesRepository, { provide: DRIZZLE, useValue: db }], @@ -95,13 +120,15 @@ describe('BranchesRepository', () => { }); it('findById maps a row to domain Branch', async () => { - limit.mockResolvedValueOnce([row]); const branch = await repository.findById('br-1'); expect(branch).toMatchObject({ id: 'br-1', code: 'JKT_01', name: 'Jakarta Pusat', createdBy: 'user-1', + division: { id: 'div-1', code: 'JKT', name: 'Jakarta' }, + createdByUser: { id: 'user-1', username: 'admin' }, + updatedByUser: { id: 'user-1', username: 'admin' }, }); expect(branch?.phone.value).toBe('+6281234567890'); expect(branch?.status.value).toBe('draft'); @@ -109,12 +136,11 @@ describe('BranchesRepository', () => { }); it('findById returns null when missing', async () => { - limit.mockResolvedValueOnce([]); + limit.mockImplementationOnce(() => Promise.resolve([])); await expect(repository.findById('missing')).resolves.toBeNull(); }); it('findByCode maps a row', async () => { - limit.mockResolvedValueOnce([row]); const branch = await repository.findByCode('JKT_01'); expect(branch?.code).toBe('JKT_01'); }); @@ -127,17 +153,10 @@ describe('BranchesRepository', () => { }), })) .mockImplementationOnce(() => ({ - from: () => ({ - $dynamic: () => ({ - where: () => ({ - orderBy: () => ({ - limit: () => ({ - offset: () => Promise.resolve([row]), - }), - }), - }), - }), - }), + from: () => { + const chain = joinChain(); + return chain; + }, })); const result = await repository.list({ @@ -158,12 +177,18 @@ describe('BranchesRepository', () => { }); expect(result.total).toBe(1); expect(result.data[0].code).toBe('JKT_01'); + expect(result.data[0].division).toEqual({ + id: 'div-1', + code: 'JKT', + name: 'Jakarta', + }); }); it('create inserts and maps unique violations', async () => { returning.mockResolvedValueOnce([row]); const created = await repository.create(createInput); expect(created.code).toBe('JKT_01'); + expect(created.createdByUser).toEqual({ id: 'user-1', username: 'admin' }); returning.mockRejectedValueOnce({ code: '23505' }); await expect(repository.create(createInput)).rejects.toBeInstanceOf( @@ -209,17 +234,17 @@ describe('BranchesRepository', () => { 'user-1', ); expect(updated.id).toBe('br-1'); + expect(updated.updatedByUser).toEqual({ id: 'user-1', username: 'admin' }); }); it('update throws when missing', async () => { - limit.mockResolvedValueOnce([]); + limit.mockImplementationOnce(() => Promise.resolve([])); await expect( repository.update('missing', { userId: 'user-1' }), ).rejects.toBeInstanceOf(NotFoundException); }); it('update maps a row when present', async () => { - limit.mockResolvedValueOnce([row]); returning.mockResolvedValueOnce([row]); const updated = await repository.update('br-1', { name: 'Jakarta Selatan', diff --git a/src/modules/configuration/branches/branches.repository.ts b/src/modules/configuration/branches/branches.repository.ts index db5491a..9321bc8 100644 --- a/src/modules/configuration/branches/branches.repository.ts +++ b/src/modules/configuration/branches/branches.repository.ts @@ -6,6 +6,7 @@ import { NotFoundException, } from '@nestjs/common'; import { and, asc, count, eq, ilike, inArray, or, SQL } from 'drizzle-orm'; +import { alias } from 'drizzle-orm/pg-core'; import { DateTime } from '../../../common/value-objects/date-time/date-time'; import { PhoneNumber } from '../../../common/value-objects/phone-number/phone-number'; import { Status } from '../../../common/value-objects/status/status'; @@ -15,6 +16,7 @@ import { type BranchRow, type NewBranchRow, } from '../../../database/branches-table'; +import { divisions, users } from '../../../database/schema'; import type { Branch, CreateBranchInput, @@ -22,6 +24,16 @@ import type { UpdateBranchInput, } from './branch'; +const createdByUsers = alias(users, 'created_by_users'); +const updatedByUsers = alias(users, 'updated_by_users'); + +type BranchJoinedRow = { + branch: BranchRow; + division: typeof divisions.$inferSelect | null; + createdByUser: typeof users.$inferSelect | null; + updatedByUser: typeof users.$inferSelect | null; +}; + @Injectable() export class BranchesRepository { constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) {} @@ -36,7 +48,7 @@ export class BranchesRepository { .where(where); const totalRow = totalRows[0]; - let qb = this.db.select().from(branches).$dynamic(); + let qb = this.selectWithRelations().$dynamic(); qb = this.extendListQuery(qb, filters); const rows = await qb .where(where) @@ -59,9 +71,7 @@ export class BranchesRepository { } async findById(id: string): Promise { - const rows: BranchRow[] = await this.db - .select() - .from(branches) + const rows = await this.selectWithRelations() .where(eq(branches.id, id)) .limit(1); const row = rows[0]; @@ -69,9 +79,7 @@ export class BranchesRepository { } async findByCode(code: string): Promise { - const rows = await this.db - .select() - .from(branches) + const rows = await this.selectWithRelations() .where(eq(branches.code, code)) .limit(1); const row = rows[0]; @@ -87,7 +95,7 @@ export class BranchesRepository { .values(this.toInsertValues(input, status, now, input.userId)) .returning(); const row = inserted[0]; - return this.toDomain(row); + return this.requireById(row.id); } catch (error) { this.rethrowConstraintViolation(error); } @@ -151,7 +159,7 @@ export class BranchesRepository { if (!row) { throw new NotFoundException('Branch not found'); } - return this.toDomain(row); + return this.requireById(row.id); } catch (error) { this.rethrowConstraintViolation(error); } @@ -176,7 +184,7 @@ export class BranchesRepository { if (!row) { throw new NotFoundException('Branch not found'); } - return this.toDomain(row); + return this.requireById(row.id); } async bulkUpdateStatus( @@ -221,6 +229,28 @@ export class BranchesRepository { return deleted.length; } + private selectWithRelations() { + return this.db + .select({ + branch: branches, + division: divisions, + createdByUser: createdByUsers, + updatedByUser: updatedByUsers, + }) + .from(branches) + .leftJoin(divisions, eq(branches.divisionId, divisions.id)) + .leftJoin(createdByUsers, eq(branches.createdBy, createdByUsers.id)) + .leftJoin(updatedByUsers, eq(branches.updatedBy, updatedByUsers.id)); + } + + private async requireById(id: string): Promise { + const loaded = await this.findById(id); + if (!loaded) { + throw new NotFoundException('Branch not found'); + } + return loaded; + } + private buildListWhere(filters: ListBranchesFilters): SQL | undefined { const parts: SQL[] = []; if (filters.code) { @@ -299,29 +329,52 @@ export class BranchesRepository { }; } - private toDomain(row: BranchRow): Branch { + private toDomain(row: BranchJoinedRow): Branch { + const branch = row.branch; return { - id: row.id, - code: row.code, - name: row.name, - phone: PhoneNumber.create(row.phone), - address: row.address, - latitude: row.latitude, - longitude: row.longitude, - workingDaysStart: row.workingDaysStart, - workingDaysEnd: row.workingDaysEnd, - workingHoursStart: row.workingHoursStart, - workingHoursEnd: row.workingHoursEnd, - nfcId: row.nfcId, - divisionId: row.divisionId, - status: Status.create(row.status), - createdAt: DateTime.fromUnixMs(row.createdAt), - updatedAt: DateTime.fromUnixMs(row.updatedAt), - createdBy: row.createdBy, - updatedBy: row.updatedBy, + id: branch.id, + code: branch.code, + name: branch.name, + phone: PhoneNumber.create(branch.phone), + address: branch.address, + latitude: branch.latitude, + longitude: branch.longitude, + workingDaysStart: branch.workingDaysStart, + workingDaysEnd: branch.workingDaysEnd, + workingHoursStart: branch.workingHoursStart, + workingHoursEnd: branch.workingHoursEnd, + nfcId: branch.nfcId, + divisionId: branch.divisionId, + status: Status.create(branch.status), + createdAt: DateTime.fromUnixMs(branch.createdAt), + updatedAt: DateTime.fromUnixMs(branch.updatedAt), + createdBy: branch.createdBy, + updatedBy: branch.updatedBy, + division: this.toDefaultRelation(row.division), + createdByUser: this.toUserRelation(row.createdByUser, branch.createdBy), + updatedByUser: this.toUserRelation(row.updatedByUser, branch.updatedBy), }; } + private toDefaultRelation( + row: { id: string; code: string; name: string } | null, + ): Branch['division'] { + if (!row?.id) { + return null; + } + return { id: row.id, code: row.code, name: row.name }; + } + + private toUserRelation( + row: { id: string; username: string } | null, + fallbackId: string, + ): Branch['createdByUser'] { + if (row?.id) { + return { id: row.id, username: row.username }; + } + return { id: fallbackId, username: '' }; + } + private rethrowConstraintViolation(error: unknown): never { const err = error as { code?: string; constraint?: string }; if (err.code === '23505') { diff --git a/src/modules/configuration/branches/branches.service.spec.ts b/src/modules/configuration/branches/branches.service.spec.ts index d83aadc..63668e7 100644 --- a/src/modules/configuration/branches/branches.service.spec.ts +++ b/src/modules/configuration/branches/branches.service.spec.ts @@ -44,6 +44,9 @@ describe('BranchesService', () => { updatedAt: now, createdBy: 'user-1', updatedBy: 'user-1', + division: { id: 'div-1', code: 'JKT', name: 'Jakarta' }, + createdByUser: { id: 'user-1', username: 'admin' }, + updatedByUser: { id: 'user-1', username: 'admin' }, }; const createInput = { @@ -92,8 +95,14 @@ describe('BranchesService', () => { phone: '+6281234567890', status: 'draft', createdAt: now.value, + division: { id: 'div-1', code: 'JKT', name: 'Jakarta' }, + createdBy: { id: 'user-1', username: 'admin' }, + updatedBy: { id: 'user-1', username: 'admin' }, }); + expect(result.data[0]).not.toHaveProperty('divisionId'); expect(service.visibleFields).toContain('phone'); + expect(service.visibleFields).toContain('division'); + expect(service.visibleFields).not.toContain('divisionId'); }); it('findById throws when missing', async () => { @@ -108,6 +117,12 @@ describe('BranchesService', () => { const result = await service.findById('br-1'); expect(result.id).toBe('br-1'); expect(result.phone).toBe('+6281234567890'); + expect(result.division).toEqual({ + id: 'div-1', + code: 'JKT', + name: 'Jakarta', + }); + expect(result.createdBy).toEqual({ id: 'user-1', username: 'admin' }); }); it('create defaults status to draft and stores E.164 phone', async () => { diff --git a/src/modules/configuration/branches/branches.service.ts b/src/modules/configuration/branches/branches.service.ts index 5930c58..951237f 100644 --- a/src/modules/configuration/branches/branches.service.ts +++ b/src/modules/configuration/branches/branches.service.ts @@ -54,7 +54,7 @@ const VISIBLE_FIELDS = [ 'workingHoursStart', 'workingHoursEnd', 'nfcId', - 'divisionId', + 'division', 'status', 'createdAt', 'updatedAt', @@ -341,15 +341,31 @@ export class BranchesService { workingHoursStart: branch.workingHoursStart, workingHoursEnd: branch.workingHoursEnd, nfcId: branch.nfcId, - divisionId: branch.divisionId, + division: this.toDivisionItem(branch.division), status: branch.status.value, createdAt: branch.createdAt.value, updatedAt: branch.updatedAt.value, - createdBy: branch.createdBy, - updatedBy: branch.updatedBy, + createdBy: this.toUserItem(branch.createdByUser), + updatedBy: this.toUserItem(branch.updatedByUser), }; } + private toDivisionItem( + division: { id: string; code: string; name: string } | null, + ): { id: string; code: string; name: string } | null { + if (division == null) { + return null; + } + return { id: division.id, code: division.code, name: division.name }; + } + + private toUserItem(user: { id: string; username: string }): { + id: string; + username: string; + } { + return { id: user.id, username: user.username }; + } + get visibleFields(): readonly string[] { return VISIBLE_FIELDS; } diff --git a/src/modules/configuration/branches/dto/branch.dto.ts b/src/modules/configuration/branches/dto/branch.dto.ts index 12e5af0..943e37d 100644 --- a/src/modules/configuration/branches/dto/branch.dto.ts +++ b/src/modules/configuration/branches/dto/branch.dto.ts @@ -14,7 +14,11 @@ import { MaxLength, Min, } from 'class-validator'; -import { PaginationQueryDto } from '../../../../common/http/response'; +import { + DefaultRelationDto, + PaginationQueryDto, + UserRelationDto, +} from '../../../../common/http/response'; import { CORE_STATUSES } from '../../../../common/value-objects/status/status'; import { BRANCH_ADDRESS_MAX_LENGTH, @@ -320,8 +324,8 @@ export class BranchDto { @ApiPropertyOptional({ nullable: true }) nfcId!: string | null; - @ApiPropertyOptional({ format: 'uuid', nullable: true }) - divisionId!: string | null; + @ApiPropertyOptional({ type: DefaultRelationDto, nullable: true }) + division!: DefaultRelationDto | null; @ApiProperty({ enum: CORE_STATUSES }) status!: string; @@ -332,9 +336,9 @@ export class BranchDto { @ApiProperty({ description: 'Unix ms' }) updatedAt!: number; - @ApiProperty({ format: 'uuid' }) - createdBy!: string; + @ApiProperty({ type: UserRelationDto }) + createdBy!: UserRelationDto; - @ApiProperty({ format: 'uuid' }) - updatedBy!: string; + @ApiProperty({ type: UserRelationDto }) + updatedBy!: UserRelationDto; } diff --git a/test/branches.e2e-spec.ts b/test/branches.e2e-spec.ts index 736d7f5..4555fee 100644 --- a/test/branches.e2e-spec.ts +++ b/test/branches.e2e-spec.ts @@ -26,6 +26,8 @@ describe('Branches (e2e)', () => { let adminUserId: string; let otherAccessToken: string; let divisionId: string; + let divisionCode: string; + const divisionName = 'Jakarta Division'; const payload = { code: `JKT_${Date.now().toString().slice(-6)}`, @@ -103,11 +105,12 @@ describe('Branches (e2e)', () => { .post('/divisions') .set('Authorization', `Bearer ${adminAccessToken}`) .send({ - name: 'Jakarta Division', + name: divisionName, code: `JD_${Date.now().toString().slice(-6)}`, }) .expect(201); divisionId = (division.body as { id: string }).id; + divisionCode = (division.body as { code: string }).code; }); afterAll(async () => { @@ -137,11 +140,13 @@ describe('Branches (e2e)', () => { name: 'Jakarta Pusat', phone: '+6281234567890', status: 'draft', - createdBy: adminUserId, + createdBy: { id: adminUserId, username: adminUsername }, + updatedBy: { id: adminUserId, username: adminUsername }, latitude: null, nfcId: null, - divisionId: null, + division: null, }); + expect(created.body).not.toHaveProperty('divisionId'); const id = (created.body as { id: string }).id; await request(app.getHttpServer()) @@ -167,7 +172,7 @@ describe('Branches (e2e)', () => { .set('Authorization', `Bearer ${adminAccessToken}`) .expect(200); - await request(app.getHttpServer()) + const patched = await request(app.getHttpServer()) .patch(`/branches/${id}`) .set('Authorization', `Bearer ${adminAccessToken}`) .send({ @@ -178,6 +183,15 @@ describe('Branches (e2e)', () => { divisionId, }) .expect(200); + expect(patched.body).toMatchObject({ + name: 'Jakarta Selatan', + division: { + id: divisionId, + code: divisionCode, + name: divisionName, + }, + createdBy: { id: adminUserId, username: adminUsername }, + }); await request(app.getHttpServer()) .patch(`/branches/${id}`)