Просмотр исходного кода

feat: expose customer simulation gallery endpoint

ChenYu 4 месяцев назад
Родитель
Сommit
fc0ad175b9

+ 28 - 2
backend/src/modules/simulations/simulations.controller.ts

@@ -2,6 +2,7 @@ import {
   Body,
   Controller,
   Get,
+  Param,
   Post,
   Query,
   Req,
@@ -36,7 +37,32 @@ export class SimulationsController {
   }
 
   @Post()
-  create(@Body() body: CreateSimulationDto) {
-    return this.simulationsService.create(body);
+  create(@Req() request: AuthenticatedRequest, @Body() body: CreateSimulationDto) {
+    return this.simulationsService.createSimulation(
+      BigInt(request.user.stylistId),
+      body,
+    );
+  }
+
+  @Get('customer/:customerId/gallery')
+  gallery(
+    @Req() request: AuthenticatedRequest,
+    @Param('customerId') customerId: string,
+  ) {
+    return this.simulationsService.listCustomerSimulationGallery(
+      BigInt(request.user.stylistId),
+      customerId,
+    );
+  }
+
+  @Get(':simulationId')
+  detail(
+    @Req() request: AuthenticatedRequest,
+    @Param('simulationId') simulationId: string,
+  ) {
+    return this.simulationsService.getSimulationDetail(
+      BigInt(request.user.stylistId),
+      simulationId,
+    );
   }
 }

+ 264 - 0
backend/test/simulations.e2e-spec.ts

@@ -0,0 +1,264 @@
+import { INestApplication } from '@nestjs/common';
+import { JwtService } from '@nestjs/jwt';
+import { Test } from '@nestjs/testing';
+import * as request from 'supertest';
+import { configureApp } from '../src/app.setup';
+import { AppModule } from '../src/app.module';
+import { AiSimulationClient } from '../src/modules/simulations/ai-simulation.client';
+import { PrismaService } from '../src/prisma/prisma.service';
+
+describe('Simulations (e2e)', () => {
+  let app: INestApplication;
+  let jwtService: JwtService;
+  let prisma: PrismaService;
+  const execute = jest.fn();
+  const getResult = jest.fn();
+
+  async function signToken() {
+    return jwtService.signAsync({
+      sub: 1,
+      stylistId: '1',
+      email: 'stylist@example.com',
+      username: 'Ogawa',
+    });
+  }
+
+  beforeAll(async () => {
+    execute.mockReset();
+    getResult.mockReset();
+
+    const moduleRef = await Test.createTestingModule({
+      imports: [AppModule],
+    })
+      .overrideProvider(AiSimulationClient)
+      .useValue({ execute, getResult })
+      .compile();
+
+    app = moduleRef.createNestApplication();
+    configureApp(app);
+    await app.init();
+    jwtService = app.get(JwtService);
+    prisma = app.get(PrismaService);
+  });
+
+  afterAll(async () => {
+    await prisma.aiHairSimulationTask.deleteMany({
+      where: {
+        taskId: {
+          startsWith: 'sim-e2e-task-',
+        },
+      },
+    });
+    await prisma.aiHairSimulation.deleteMany({
+      where: {
+        simulateRemark: {
+          startsWith: 'sim-e2e-',
+        },
+      },
+    });
+    await prisma.hairTemplate.deleteMany({
+      where: {
+        templateName: {
+          startsWith: 'sim-e2e-',
+        },
+      },
+    });
+    await prisma.customer.deleteMany({
+      where: {
+        name: {
+          startsWith: 'sg-',
+        },
+      },
+    });
+    await app.close();
+  });
+
+  it('/api/v1/simulations (POST) creates an anonymous simulation', async () => {
+    const token = await signToken();
+    const uniqueSuffix = Date.now();
+    const template = await prisma.hairTemplate.create({
+      data: {
+        templateName: `sim-e2e-template-${uniqueSuffix}`,
+        coverUrl: '',
+        status: 'PUBLIC',
+        category: 'SHORT',
+        tags: '',
+        description: null,
+        modelUrl: '',
+        clothesUrl: '',
+        backgroundUrl: '',
+        refHairUrl: '',
+        hairstylistId: BigInt(1),
+        templateUrl: `https://cdn.example.com/${uniqueSuffix}.zip`,
+        isSystemTemplate: 'NO',
+      } as never,
+    });
+    execute.mockResolvedValueOnce({
+      taskId: `sim-e2e-task-${uniqueSuffix}`,
+    });
+
+    const response = await request(app.getHttpServer())
+      .post('/api/v1/simulations')
+      .set('Authorization', `Bearer ${token}`)
+      .send({
+        originImage: 'https://cdn.example.com/origin.jpg',
+        templateIds: [template.id.toString()],
+        simulateRemark: `sim-e2e-${uniqueSuffix}`,
+      });
+
+    expect(response.status).toBe(201);
+
+    expect(execute).toHaveBeenCalledWith(
+      template.templateUrl,
+      'https://cdn.example.com/origin.jpg',
+    );
+    expect(response.body.data).toEqual({
+      simulationId: expect.any(String),
+    });
+  });
+
+  it('/api/v1/simulations/:id (GET) returns task detail for the owner', async () => {
+    const token = await signToken();
+    const uniqueSuffix = `${Date.now()}-detail`;
+    const template = await prisma.hairTemplate.create({
+      data: {
+        templateName: `sim-e2e-template-${uniqueSuffix}`,
+        coverUrl: 'https://cdn.example.com/cover.png',
+        status: 'PUBLIC',
+        category: 'MEDIUM',
+        tags: 'airy',
+        description: 'detail template',
+        modelUrl: '',
+        clothesUrl: '',
+        backgroundUrl: '',
+        refHairUrl: '',
+        hairstylistId: BigInt(1),
+        templateUrl: `https://cdn.example.com/${uniqueSuffix}.zip`,
+        isSystemTemplate: 'NO',
+      } as never,
+    });
+    execute.mockResolvedValueOnce({
+      taskId: `sim-e2e-task-${uniqueSuffix}`,
+    });
+
+    const created = await request(app.getHttpServer())
+      .post('/api/v1/simulations')
+      .set('Authorization', `Bearer ${token}`)
+      .send({
+        originImage: 'https://cdn.example.com/origin-detail.jpg',
+        templateIds: [template.id.toString()],
+        simulateRemark: `sim-e2e-${uniqueSuffix}`,
+      })
+      .expect(201);
+
+    const response = await request(app.getHttpServer())
+      .get(`/api/v1/simulations/${created.body.data.simulationId}`)
+      .set('Authorization', `Bearer ${token}`)
+      .expect(200);
+
+    expect(response.body.data).toMatchObject({
+      id: created.body.data.simulationId,
+      createdAt: expect.any(String),
+      originImage: 'https://cdn.example.com/origin-detail.jpg',
+      totalTaskCount: 1,
+      processingTaskCount: 1,
+      successTaskCount: 0,
+      failedTaskCount: 0,
+      overallStatus: 'PROCESSING',
+      tasks: [
+        expect.objectContaining({
+          templateId: template.id.toString(),
+          templateName: template.templateName,
+          templateCategory: template.category,
+          templateSource: 'STYLIST',
+          taskStatus: 'PROCESSING',
+          resultImages: [],
+        }),
+      ],
+    });
+  });
+
+  it('/api/v1/simulations/customer/:customerId/gallery (GET) returns flattened result cards', async () => {
+    const token = await signToken();
+    const uniqueSuffix = `${Date.now()}-gallery`;
+    const customer = await prisma.customer.create({
+      data: {
+        hairstylistId: BigInt(1),
+        name: `sg-${Date.now()}`,
+        namePronunciation: '',
+        phone: `090-${String(Date.now()).slice(-4)}-3333`,
+        email: '',
+        avatar: 'https://cdn.example.com/customer.png',
+        expectContact: 'LINE',
+        planDate: new Date(),
+        nextFollowDate: new Date(),
+      } as never,
+    });
+    const template = await prisma.hairTemplate.create({
+      data: {
+        templateName: `sim-e2e-template-${uniqueSuffix}`,
+        coverUrl: 'https://cdn.example.com/cover.png',
+        status: 'PUBLIC',
+        category: 'SHORT',
+        tags: 'airy',
+        description: 'gallery template',
+        modelUrl: '',
+        clothesUrl: '',
+        backgroundUrl: '',
+        refHairUrl: '',
+        hairstylistId: BigInt(1),
+        templateUrl: `https://cdn.example.com/${uniqueSuffix}.zip`,
+        isSystemTemplate: 'NO',
+      } as never,
+    });
+    execute.mockResolvedValueOnce({
+      taskId: `sim-e2e-task-${uniqueSuffix}`,
+    });
+
+    const created = await request(app.getHttpServer())
+      .post('/api/v1/simulations')
+      .set('Authorization', `Bearer ${token}`)
+      .send({
+        customerId: customer.id.toString(),
+        originImage: 'https://cdn.example.com/origin-gallery.jpg',
+        templateIds: [template.id.toString()],
+        simulateRemark: `sim-e2e-${uniqueSuffix}`,
+      })
+      .expect(201);
+
+    await prisma.aiHairSimulationTask.updateMany({
+      where: {
+        simulationId: BigInt(created.body.data.simulationId),
+      },
+      data: {
+        taskStatus: 'SUCCESSES',
+        resultImages: JSON.stringify([
+          'https://cdn.example.com/gallery-2.png',
+          'https://cdn.example.com/gallery-1.png',
+        ]),
+      } as never,
+    });
+
+    const response = await request(app.getHttpServer())
+      .get(`/api/v1/simulations/customer/${customer.id}/gallery`)
+      .set('Authorization', `Bearer ${token}`);
+
+    expect(response.status).toBe(200);
+    expect(response.body.data.items).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          simulationId: created.body.data.simulationId,
+          imageUrl: 'https://cdn.example.com/gallery-2.png',
+          templateName: template.templateName,
+          taskStatus: 'SUCCESSES',
+        }),
+        expect.objectContaining({
+          simulationId: created.body.data.simulationId,
+          imageUrl: 'https://cdn.example.com/gallery-1.png',
+          templateName: template.templateName,
+          taskStatus: 'SUCCESSES',
+        }),
+      ]),
+    );
+  });
+});