- Replaced the previous hydration logic with a new method, `loadChildrenForRows`, to efficiently load related data for plans in a single query. - Implemented a `groupByPlanId` utility to organize related entities by plan ID, improving data retrieval performance. - Updated error handling in the `getPlan` method to throw a `NotFoundException` if a plan is not found. - Modified end-to-end tests to validate the new structure of the response, ensuring that related destinations are correctly included in the API output.
298 lines
8.9 KiB
TypeScript
298 lines
8.9 KiB
TypeScript
import { INestApplication } from '@nestjs/common';
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { eq } from 'drizzle-orm';
|
|
import request from 'supertest';
|
|
import { App } from 'supertest/types';
|
|
import { AppModule } from '../src/app.module';
|
|
import { configureApp } from '../src/common/configure-app';
|
|
import { DRIZZLE, type DrizzleDB } from '../src/database/database.module';
|
|
import {
|
|
privilegeDetails,
|
|
privilegeKeys,
|
|
privileges,
|
|
users,
|
|
} from '../src/database/schema';
|
|
import { PRIVILEGE_ACTIONS } from '../src/modules/privileges/privilege-action';
|
|
import { registerAndActivate } from './helpers/activate-user';
|
|
import { DateTime } from '../src/common/value-objects/date-time/date-time';
|
|
|
|
const MS_PER_DAY = 86_400_000;
|
|
|
|
function nextMondayRange(): { from: string; to: string } {
|
|
const start = DateTime.fromUnixMs(Math.trunc(Date.now())).startOfDay();
|
|
const mondays: string[] = [];
|
|
for (let offset = 0; offset < 21 && mondays.length < 2; offset += 1) {
|
|
const day = DateTime.fromUnixMs(
|
|
start.value + offset * MS_PER_DAY,
|
|
).startOfDay();
|
|
if (day.weekdayName() === 'monday') {
|
|
mondays.push(day.format().slice(0, 10));
|
|
}
|
|
}
|
|
return { from: mondays[0], to: mondays[1] };
|
|
}
|
|
|
|
describe('Plans (e2e)', () => {
|
|
let app: INestApplication<App>;
|
|
let db: DrizzleDB;
|
|
|
|
const password = 'password123';
|
|
const adminUsername = `pln_admin_${Date.now()}`;
|
|
const otherUsername = `pln_other_${Date.now()}`;
|
|
|
|
let adminAccessToken: string;
|
|
let adminUserId: string;
|
|
let otherAccessToken: string;
|
|
let employeeId: string;
|
|
let startBranchId: string;
|
|
let endBranchId: string;
|
|
let customerId: string;
|
|
let extraCustomerId: string;
|
|
let cycleId: string;
|
|
|
|
beforeAll(async () => {
|
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
}).compile();
|
|
|
|
app = moduleFixture.createNestApplication();
|
|
configureApp(app, {
|
|
NODE_ENV: 'test',
|
|
SWAGGER_ENABLED: 'false',
|
|
});
|
|
await app.init();
|
|
db = app.get(DRIZZLE);
|
|
|
|
const admin = await registerAndActivate(app, db, adminUsername, password);
|
|
adminAccessToken = admin.accessToken;
|
|
adminUserId = admin.userId;
|
|
|
|
const other = await registerAndActivate(app, db, otherUsername, password);
|
|
otherAccessToken = other.accessToken;
|
|
|
|
const now = Date.now();
|
|
const [priv] = await db
|
|
.insert(privileges)
|
|
.values({
|
|
name: 'Plan Admin',
|
|
code: `PLN_ADMIN_${now}`,
|
|
status: 'active',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
createdBy: adminUserId,
|
|
updatedBy: adminUserId,
|
|
})
|
|
.returning();
|
|
|
|
const keys = await db.select().from(privilegeKeys);
|
|
await db.insert(privilegeDetails).values(
|
|
keys.flatMap((key) =>
|
|
PRIVILEGE_ACTIONS.map((action) => ({
|
|
privilegeId: priv.id,
|
|
privilegeKeyId: key.id,
|
|
action,
|
|
value: true,
|
|
})),
|
|
),
|
|
);
|
|
await db
|
|
.update(users)
|
|
.set({ privilegeId: priv.id, updatedAt: Date.now() })
|
|
.where(eq(users.id, adminUserId));
|
|
|
|
const suffix = Date.now().toString().slice(-6);
|
|
const employee = await request(app.getHttpServer())
|
|
.post('/employees')
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({
|
|
code: `PLN_E_${suffix}`,
|
|
name: 'Ada Lovelace',
|
|
phone: '+6281234567890',
|
|
position: 'sales',
|
|
})
|
|
.expect(201);
|
|
employeeId = (employee.body as { id: string }).id;
|
|
|
|
const start = await request(app.getHttpServer())
|
|
.post('/branches')
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({
|
|
code: `PLN_S_${suffix}`,
|
|
name: 'Start Branch',
|
|
phone: '+6281234567890',
|
|
address: 'Jl Sudirman No 1',
|
|
latitude: -6.2,
|
|
longitude: 106.8,
|
|
workingDaysStart: 'monday',
|
|
workingDaysEnd: 'friday',
|
|
workingHoursStart: '08:00',
|
|
workingHoursEnd: '17:00',
|
|
})
|
|
.expect(201);
|
|
startBranchId = (start.body as { id: string }).id;
|
|
|
|
const end = await request(app.getHttpServer())
|
|
.post('/branches')
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({
|
|
code: `PLN_N_${suffix}`,
|
|
name: 'End Branch',
|
|
phone: '+6281234567891',
|
|
address: 'Jl Thamrin No 2',
|
|
latitude: -6.21,
|
|
longitude: 106.81,
|
|
workingDaysStart: 'monday',
|
|
workingDaysEnd: 'friday',
|
|
workingHoursStart: '08:00',
|
|
workingHoursEnd: '17:00',
|
|
})
|
|
.expect(201);
|
|
endBranchId = (end.body as { id: string }).id;
|
|
|
|
const customer = await request(app.getHttpServer())
|
|
.post('/customers')
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({
|
|
code: `PLN_C_${suffix}`,
|
|
name: 'Acme Corp',
|
|
phone: '+6281234567892',
|
|
address: 'Jl Gatot No 3',
|
|
latitude: -6.22,
|
|
longitude: 106.82,
|
|
})
|
|
.expect(201);
|
|
customerId = (customer.body as { id: string }).id;
|
|
|
|
const extra = await request(app.getHttpServer())
|
|
.post('/customers')
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({
|
|
code: `PLN_D_${suffix}`,
|
|
name: 'Beta Corp',
|
|
phone: '+6281234567893',
|
|
address: 'Jl Rasuna No 4',
|
|
latitude: -6.23,
|
|
longitude: 106.83,
|
|
})
|
|
.expect(201);
|
|
extraCustomerId = (extra.body as { id: string }).id;
|
|
|
|
await request(app.getHttpServer())
|
|
.patch('/settings')
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({ cycleStartDate: '2026-01-05' })
|
|
.expect(200);
|
|
|
|
const cycle = await request(app.getHttpServer())
|
|
.post('/cycles')
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({
|
|
employeeId,
|
|
purpose: 'sales',
|
|
cycleNumber: 1,
|
|
weekdays: {
|
|
monday: {
|
|
startBranchId,
|
|
endBranchId,
|
|
customerIds: [customerId],
|
|
},
|
|
},
|
|
})
|
|
.expect(201);
|
|
cycleId = (cycle.body as { id: string }).id;
|
|
|
|
await request(app.getHttpServer())
|
|
.patch(`/cycles/${cycleId}/status`)
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({ status: 'active' })
|
|
.expect(200);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
it('forbids plans list without permission', async () => {
|
|
await request(app.getHttpServer())
|
|
.get('/plans')
|
|
.set('Authorization', `Bearer ${otherAccessToken}`)
|
|
.expect(403);
|
|
});
|
|
|
|
it('generates Monday plans, skips existing, and supports D-Day destination edits', async () => {
|
|
const { from, to } = nextMondayRange();
|
|
const generated = await request(app.getHttpServer())
|
|
.post('/plans/generate')
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({
|
|
employeeId,
|
|
purpose: 'sales',
|
|
from,
|
|
to,
|
|
})
|
|
.expect(201);
|
|
expect((generated.body as { created: number }).created).toBe(2);
|
|
expect((generated.body as { skipped: number }).skipped).toBeGreaterThan(0);
|
|
|
|
const again = await request(app.getHttpServer())
|
|
.post('/plans/generate')
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({
|
|
employeeId,
|
|
purpose: 'sales',
|
|
from,
|
|
to,
|
|
})
|
|
.expect(201);
|
|
expect((again.body as { created: number }).created).toBe(0);
|
|
|
|
const list = await request(app.getHttpServer())
|
|
.get('/plans')
|
|
.query({ employeeId, purpose: 'sales', date: from })
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.expect(200);
|
|
const listed = (
|
|
list.body as {
|
|
data: Array<{
|
|
id: string;
|
|
destinations: Array<{ customer: { id: string } | null }>;
|
|
}>;
|
|
}
|
|
).data;
|
|
expect(listed[0].destinations).toHaveLength(1);
|
|
expect(listed[0].destinations[0].customer?.id).toBe(customerId);
|
|
const planId = listed[0].id;
|
|
|
|
const added = await request(app.getHttpServer())
|
|
.post(`/plans/${planId}/destinations`)
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.send({ customerId: extraCustomerId })
|
|
.expect(201);
|
|
expect(
|
|
(added.body as { destinations: unknown[] }).destinations,
|
|
).toHaveLength(2);
|
|
|
|
const extraDest = (
|
|
added.body as {
|
|
destinations: Array<{ id: string; customer: { id: string } | null }>;
|
|
}
|
|
).destinations.find(
|
|
(destination) => destination.customer?.id === extraCustomerId,
|
|
);
|
|
const removed = await request(app.getHttpServer())
|
|
.delete(`/plans/${planId}/destinations/${extraDest?.id}`)
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.expect(200);
|
|
expect(
|
|
(removed.body as { destinations: unknown[] }).destinations,
|
|
).toHaveLength(1);
|
|
|
|
const lastId = (removed.body as { destinations: Array<{ id: string }> })
|
|
.destinations[0].id;
|
|
await request(app.getHttpServer())
|
|
.delete(`/plans/${planId}/destinations/${lastId}`)
|
|
.set('Authorization', `Bearer ${adminAccessToken}`)
|
|
.expect(400);
|
|
});
|
|
});
|