| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- import { TeachingCourseCategory, TeachingRule } from '../admin.models';
- import { TEACHING_STAGE_OPTIONS, teachingRateMatrix } from './operations-payroll.component';
- describe('operations payroll teaching standards', () => {
- it('uses the three standard school stages when presenting database course ancestry', () => {
- expect(TEACHING_STAGE_OPTIONS.map((stage) => stage.name)).toEqual(['小学', '初中', '高中']);
- });
- it('builds the current online and offline price matrix without using an older version', () => {
- const categories = [
- { key: 'trial', name: '体验训练', sortOrder: 10, active: true },
- { key: 'word_self_study', name: '单词自学课', sortOrder: 20, active: true },
- ] as TeachingCourseCategory[];
- const rule = (categoryKey: string, deliveryMode: 'online' | 'offline', teacherPay: number, effectiveFrom: string): TeachingRule => ({
- objectId: `${categoryKey}:${deliveryMode}:${effectiveFrom}`,
- categoryKey,
- categoryName: categories.find((category) => category.key === categoryKey)?.name || categoryKey,
- durationMinutes: 60,
- deliveryMode,
- deliveryLabel: deliveryMode === 'online' ? '线上' : '线下',
- teacherPay,
- creditCosts: [],
- effectiveFrom,
- active: true,
- updatedAt: '',
- });
- const matrix = teachingRateMatrix([
- rule('trial', 'online', 40, '2026-09-01'),
- rule('trial', 'online', 30, '2026-09-09'),
- rule('trial', 'offline', 40, '2026-09-09'),
- rule('word_self_study', 'online', 20, '2026-09-09'),
- rule('word_self_study', 'offline', 30, '2026-09-09'),
- ], categories, '2026-09-09');
- expect(matrix.map((row) => [row.categoryName, row.durationMinutes, row.online?.teacherPay, row.offline?.teacherPay])).toEqual([
- ['体验训练', 60, 30, 40],
- ['单词自学课', 60, 20, 30],
- ]);
- });
- });
- import { TestBed } from '@angular/core/testing';
- import { OperationsPayrollComponent } from './operations-payroll.component';
- import { OperationsService } from '../operations.service';
- import { AdminSessionService } from '../admin-session.service';
- import { PayrollOverview, PayrollSummary } from '../admin.models';
- describe('payroll interactions', () => {
- let component:OperationsPayrollComponent;
- let service:jasmine.SpyObj<OperationsService>;
- const overview=(month:string)=>({month,lines:[],coachItems:[],sourceStatus:{available:true,mode:'parse'},stores:[]} as unknown as PayrollOverview);
- beforeEach(()=>{
- service=jasmine.createSpyObj('OperationsService',['payrollOverview','payrolls','payRates','teachingStandards','request','mutate','payrollDetail']);
- service.payrollOverview.and.resolveTo(overview('2026-09'));service.payrolls.and.resolveTo({items:[],total:0,page:1,pageSize:20});service.payRates.and.resolveTo({items:[],total:0,page:1,pageSize:100});
- service.teachingStandards.and.resolveTo({categories:[],rules:[],mappings:[],summary:{courseCount:0,confirmedCount:0,unmappedCount:0,activeRuleCount:0},refreshedAt:''});service.request.and.resolveTo({items:[]});
- TestBed.configureTestingModule({providers:[{provide:OperationsService,useValue:service},{provide:AdminSessionService,useValue:{session:()=>({operationsRole:'ops-manager'})}}]});
- component=TestBed.runInInjectionContext(()=>new OperationsPayrollComponent());
- });
- it('ignores an older query response after month changes',async()=>{
- let resolveOld!:(v:PayrollOverview)=>void;service.payrollOverview.and.returnValues(new Promise(r=>resolveOld=r),Promise.resolve(overview('2026-10')));
- const old=component.load();component.month.set('2026-10');await component.load();resolveOld(overview('2026-09'));await old;expect(component.overview()?.month).toBe('2026-10');
- });
- it('reset keeps month and clears search, store and the teacher deep link',async()=>{
- component.month.set('2026-08');component.storeId.set(10);component.search.set('老师');component.rewardCoachId.set(30);component.openRewards();await component.resetFilters();expect(component.rewardFilters()).toEqual(jasmine.objectContaining({month:'2026-08',storeId:0,search:'',coachId:0,resetRevision:1}));
- });
- it('selects payroll months through the shared calendar-style picker',()=>{
- component.month.set('2026-09');component.message.set('2026年9月全公司费用草稿已生成');component.detail.set({objectId:'old-month'} as never);const event=new Event('click');spyOn(event,'stopPropagation');component.toggleMonthPicker(event);expect(component.monthPickerOpen()).toBeTrue();expect(component.monthPickerYear()).toBe(2026);component.shiftMonthPickerYear(1,event);component.selectMonth(0);expect(component.month()).toBe('2027-01');expect(component.monthLabel()).toBe('2027年1月');expect(component.monthPickerOpen()).toBeFalse();expect(component.message()).toBe('');expect(component.detail()).toBeNull();
- });
- it('retains adjustment request identity after a lost response and sends batch version',async()=>{
- component.detail.set({objectId:'batch',version:3,status:'draft'} as never);component.adjustmentCoachId.set(30);component.adjustmentAmount.set(10);component.adjustmentReason.set('补计费用');service.mutate.and.rejectWith(new Error('网络中断'));
- await component.addAdjustment();await component.addAdjustment();const calls=service.mutate.calls.allArgs();expect(calls.length).toBe(2);expect(calls[0][5]).toBe(calls[1][5]);expect(calls[0][3]).toEqual(jasmine.objectContaining({version:3,amount:10,coachId:30}));
- });
- it('does not submit an adjustment twice while saving',async()=>{
- component.saving.set(true);await component.addAdjustment();expect(service.mutate).not.toHaveBeenCalled();
- });
- it('loads batch pages independently of income and teacher filters',async()=>{
- await component.loadBatchPage(2);expect(service.payrolls).toHaveBeenCalledWith(2,20,'');component.allBatchMonths.set(false);component.month.set('2026-08');await component.loadBatchPage(1);expect(service.payrolls).toHaveBeenCalledWith(1,20,'2026-08');
- });
- it('keeps successful regions when a secondary request fails',async()=>{
- service.teachingStandards.and.rejectWith(new Error('标准服务暂不可用'));
- await component.load();expect(component.overview()?.month).toBe('2026-09');expect(component.error()).toContain('费用标准');expect(component.error()).toContain('保留上次成功结果');expect(component.loading()).toBeFalse();
- });
- it('explains missing writing evidence before submitting a mapping',async()=>{
- const row={appointmentId:'appointment',appointmentGeneralId:1,studentName:'灵灵',courseName:'阅读写作',startsAt:'2026-09-02 19:30',grade:'',categoryKey:'primary_writing',suggestedCategoryKey:'primary_writing',evidence:'',updatedAt:'2026-09-02T12:00:00.000Z'};
- await component.saveWritingMapping(row);
- expect(service.mutate).not.toHaveBeenCalled();
- expect(component.error()).toContain('请填写“灵灵”的核对依据');
- });
- it('submits a writing mapping once its category and evidence are complete',async()=>{
- const row={appointmentId:'appointment',appointmentGeneralId:1,studentName:'灵灵',courseName:'阅读写作',startsAt:'2026-09-02 19:30',grade:'',categoryKey:'primary_writing',suggestedCategoryKey:'primary_writing',evidence:' 已核实为小学 ',updatedAt:'2026-09-02T12:00:00.000Z'};
- service.mutate.and.resolveTo({updated:true});
- await component.saveWritingMapping(row);
- expect(service.mutate).toHaveBeenCalledWith('ops/course-mappings/student-commit','appointment','按学员记录核对语法写作分类',{categoryKey:'primary_writing',evidence:'已核实为小学'},'2026-09-02T12:00:00.000Z');
- expect(service.request).toHaveBeenCalledWith('ops/course-mappings/students',{month:component.month()});
- expect(service.payrollOverview).toHaveBeenCalled();
- expect(component.message()).toBe('该预约的学段分类已确认,费用统计已刷新');
- });
- it('ignores an older batch page response',async()=>{
- let resolveOld!:(v:unknown)=>void;service.payrolls.and.returnValues(new Promise(r=>resolveOld=r) as never,Promise.resolve({items:[],total:50,page:3,pageSize:20}));
- const old=component.loadBatchPage(2);await component.loadBatchPage(3);resolveOld({items:[],total:50,page:2,pageSize:20});await old;expect(component.page()?.page).toBe(3);
- });
- it('does not create a payroll draft when the legacy source is not settled',async()=>{
- component.overview.set(overview('2026-09'));
- await component.create();expect(service.mutate).not.toHaveBeenCalled();expect(component.error()).toContain('旧系统数据尚未追平');
- });
- });
|