// MaintenanceService 单测(AC-11):3 个修复型 SP(幂等可重跑)+ 2 个迁移型 SP(BUG#3 修正)。 // // 测试栈:glebarez/sqlite 文件库(真实事务)。每条"反直觉口径"都配一条反向断言。 package service import ( "context" "errors" "testing" "time" "gorm.io/gorm" "yuansiwei/unified-backend/internal/academic/apperr" "yuansiwei/unified-backend/internal/academic/model" "yuansiwei/unified-backend/internal/academic/money" infraerrors "yuansiwei/unified-backend/internal/infra/errors" ) // int64Ptr 返回 int64 指针(可空列用)。 func int64Ptr(v int64) *int64 { return &v } // TestMaintenanceRequiresSuperAdmin 维护端点(61~63)仅超管可执行(1004)。 func TestMaintenanceRequiresSuperAdmin(t *testing.T) { db := newLearningDB(t) svc := NewMaintenanceService(db, campusScope()) ctx := context.Background() _, err := svc.BackfillCoursesMaxPeople(ctx) requireAppCode(t, err, infraerrors.ErrUnscoped.Code) _, err = svc.BackfillCoursesCampus(ctx) requireAppCode(t, err, infraerrors.ErrUnscoped.Code) _, err = svc.RebuildActivityUsedLog(ctx) requireAppCode(t, err, infraerrors.ErrUnscoped.Code) _, err = svc.ReplayActivitySigned(ctx) requireAppCode(t, err, infraerrors.ErrUnscoped.Code) // SP27 是业务动作(AC-08 链路),不做超管断言:普通校区角色可执行 if _, err := svc.CounteractArrears(ctx, CounteractArrearsInput{ StudentID: "01HSTUDENT0000000000009", InstitutionID: 1, CampusID: 10, Money: money.FromCents(100), }); err != nil { t.Fatalf("SP27 不应要求超管,实际: %v", err) } } // TestBackfillCoursesMaxPeopleIdempotent SP28:max_people=0 行按分类回填;重跑不再命中(幂等)。 func TestBackfillCoursesMaxPeopleIdempotent(t *testing.T) { db := newLearningDB(t) svc := NewMaintenanceService(db, superScope()) ctx := context.Background() classify := &model.CoursesClassify{InstitutionID: 1, CampusID: 10, Name: "大班", ClassType: 1000, MaxPeople: 30, MinPeople: 1, UnitPrice: "100.00"} if err := db.Create(classify).Error; err != nil { t.Fatalf("seed 分类失败: %v", err) } zeroClassify := &model.CoursesClassify{InstitutionID: 1, CampusID: 10, Name: "未配置", ClassType: 1000, MinPeople: 1, UnitPrice: "100.00"} if err := db.Create(zeroClassify).Error; err != nil { t.Fatalf("seed 分类(0) 失败: %v", err) } // max_people 的 DDL 默认值是 1000,GORM 会忽略零值 → 显式 UPDATE 成 0(模拟"分类未配置人数") if err := db.Model(&model.CoursesClassify{}).Where("id = ?", zeroClassify.ID). UpdateColumn("max_people", 0).Error; err != nil { t.Fatalf("把分类 max_people 置 0 失败: %v", err) } c1 := &model.Courses{Name: "待回填1", InstitutionID: 1, CampusID: 10, ClassifyID: classify.ID, MaxPeople: 0} c2 := &model.Courses{Name: "待回填2", InstitutionID: 1, CampusID: 10, ClassifyID: classify.ID, MaxPeople: 0} c3 := &model.Courses{Name: "分类也0", InstitutionID: 1, CampusID: 10, ClassifyID: zeroClassify.ID, MaxPeople: 0} done := &model.Courses{Name: "已配置", InstitutionID: 1, CampusID: 10, ClassifyID: classify.ID, MaxPeople: 15} for _, row := range []*model.Courses{c1, c2, c3, done} { if err := db.Create(row).Error; err != nil { t.Fatalf("seed 课程(%s) 失败: %v", row.Name, err) } } first, err := svc.BackfillCoursesMaxPeople(ctx) if err != nil { t.Fatalf("SP28 首次执行失败: %v", err) } if first.Scanned != 3 || first.Filled != 2 || first.Skipped != 1 { t.Fatalf("首次 scanned/filled/skipped=(%d,%d,%d),期望 (3,2,1)", first.Scanned, first.Filled, first.Skipped) } if got := loadCourseMaxPeople(t, db, c1.ID); got != 30 { t.Fatalf("c1.max_people=%d,期望 30(从分类继承)", got) } if got := loadCourseMaxPeople(t, db, done.ID); got != 15 { t.Fatalf("已配置课程的 max_people 被改动:%d,期望 15", got) } if got := loadCourseMaxPeople(t, db, c3.ID); got != 0 { t.Fatalf("分类 max_people=0 时不应写非 0:%d", got) } // 反向断言:重跑不新增改动(幂等可重跑) second, err := svc.BackfillCoursesMaxPeople(ctx) if err != nil { t.Fatalf("SP28 重跑失败: %v", err) } if second.Scanned != 1 || second.Filled != 0 { t.Fatalf("重跑 scanned/filled=(%d,%d),期望 (1,0)(仅剩分类也为 0 的那条)", second.Scanned, second.Filled) } } // loadCourseMaxPeople 读课程 max_people。 func loadCourseMaxPeople(t *testing.T, db *gorm.DB, id int64) int32 { t.Helper() var row model.Courses if err := db.Where("id = ?", id).First(&row).Error; err != nil { t.Fatalf("读取课程(%d) 失败: %v", id, err) } return row.MaxPeople } // TestBackfillCoursesCampusSingleCampusOnly SP29:只有"唯一校区"的课程才回填; // 多校区 / 无排班规则 → **保持 0**(反向断言)。 func TestBackfillCoursesCampusSingleCampusOnly(t *testing.T) { db := newLearningDB(t) svc := NewMaintenanceService(db, superScope()) ctx := context.Background() single := &model.Courses{Name: "单校区", InstitutionID: 1, CampusID: 0} multi := &model.Courses{Name: "多校区", InstitutionID: 1, CampusID: 0} none := &model.Courses{Name: "无规则", InstitutionID: 1, CampusID: 0} for _, row := range []*model.Courses{single, multi, none} { if err := db.Create(row).Error; err != nil { t.Fatalf("seed 课程(%s) 失败: %v", row.Name, err) } } // single:只有校区 20 的规则(两条同一校区也算 1 个校区) seedClassRule(t, db, single.ID, 20, "-1", hm(9, 0), hm(10, 0), "100.00") seedClassRule(t, db, single.ID, 20, "-1", hm(14, 0), hm(15, 0), "100.00") // multi:校区 20 + 30 seedClassRule(t, db, multi.ID, 20, "-1", hm(9, 0), hm(10, 0), "100.00") seedClassRule(t, db, multi.ID, 30, "-1", hm(9, 0), hm(10, 0), "100.00") // none:不建规则 out, err := svc.BackfillCoursesCampus(ctx) if err != nil { t.Fatalf("SP29 执行失败: %v", err) } if out.Scanned != 3 || out.Updated != 1 || out.SkippedMultiCampus != 1 || out.SkippedNoRule != 1 { t.Fatalf("scanned/updated/multi/norule=(%d,%d,%d,%d),期望 (3,1,1,1)", out.Scanned, out.Updated, out.SkippedMultiCampus, out.SkippedNoRule) } if got := loadCourseCampus(t, db, single.ID); got != 20 { t.Fatalf("单校区课程 campus_id=%d,期望 20", got) } // 反向断言:多校区课程不回填(保持 0,否则会把机构级课程锁进某一个校区) if got := loadCourseCampus(t, db, multi.ID); got != 0 { t.Fatalf("多校区课程 campus_id=%d,期望 0(不回填)", got) } if got := loadCourseCampus(t, db, none.ID); got != 0 { t.Fatalf("无规则课程 campus_id=%d,期望 0", got) } // 重跑:只剩 multi/none 仍为 0 被扫描,Updated=0(幂等) second, err := svc.BackfillCoursesCampus(ctx) if err != nil { t.Fatalf("SP29 重跑失败: %v", err) } if second.Scanned != 2 || second.Updated != 0 { t.Fatalf("重跑 scanned/updated=(%d,%d),期望 (2,0)", second.Scanned, second.Updated) } } // loadCourseCampus 读课程 campus_id。 func loadCourseCampus(t *testing.T, db *gorm.DB, id int64) int64 { t.Helper() var row model.Courses if err := db.Where("id = ?", id).First(&row).Error; err != nil { t.Fatalf("读取课程(%d) 失败: %v", id, err) } return row.CampusID } // TestCounteractArrearsOrderCapAndDedup SP27:按课程开课时分升序核销、逐行截到剩余额度、 // 金额用完即停、余额留在账户;同 (courses,class) 多规则不重复核销(反向断言)。 func TestCounteractArrearsOrderCapAndDedup(t *testing.T) { db := newLearningDB(t) svc := NewMaintenanceService(db, campusScope()) ctx := context.Background() student := "01HSTUDENT0000000000101" confirm := "01HOPERATOR0000000000001" // 三门课:开课时分 08:00 / 10:00 / 12:00(决定核销顺序) early := &model.Courses{Name: "早课", InstitutionID: 1, CampusID: 10} mid := &model.Courses{Name: "中课", InstitutionID: 1, CampusID: 10} late := &model.Courses{Name: "晚课", InstitutionID: 1, CampusID: 10} for _, row := range []*model.Courses{early, mid, late} { if err := db.Create(row).Error; err != nil { t.Fatalf("seed 课程失败: %v", err) } } seedClassRule(t, db, early.ID, 10, "-1", hm(8, 0), hm(9, 0), "100.00") seedClassRule(t, db, mid.ID, 10, "-1", hm(10, 0), hm(11, 0), "100.00") seedClassRule(t, db, late.ID, 10, "-1", hm(12, 0), hm(13, 0), "100.00") // mid 课**两条**排班规则(同 courses+class)→ dump 的 1:N JOIN 会重复取同一报课行(去重断言) seedClassRule(t, db, mid.ID, 10, "-1", hm(18, 0), hm(19, 0), "100.00") rows := []*model.CoursesStudent{ {BizNo: "CS-A", CoursesID: early.ID, ClassID: 1, InstitutionID: 1, CampusID: 10, StudentID: student, Status: model.CoursesStudentStatusNormal, RemainderReceivable: "100.00", NetReceipts: "0.00", LessonCost: "100.00", Number: 1, ConfirmDiscountUserID: &confirm}, {BizNo: "CS-B", CoursesID: mid.ID, ClassID: 1, InstitutionID: 1, CampusID: 10, StudentID: student, Status: model.CoursesStudentStatusNormal, RemainderReceivable: "50.00", NetReceipts: "0.00", LessonCost: "100.00", Number: 1, ConfirmDiscountUserID: &confirm}, {BizNo: "CS-C", CoursesID: late.ID, ClassID: 1, InstitutionID: 1, CampusID: 10, StudentID: student, Status: model.CoursesStudentStatusNormal, RemainderReceivable: "30.00", NetReceipts: "0.00", LessonCost: "100.00", Number: 1, ConfirmDiscountUserID: &confirm}, } for _, row := range rows { if err := db.Create(row).Error; err != nil { t.Fatalf("seed 报课记录(%s) 失败: %v", row.BizNo, err) } } out, err := svc.CounteractArrears(ctx, CounteractArrearsInput{ StudentID: student, InstitutionID: 1, CampusID: 10, Money: money.FromCents(12000), // 120.00 }) if err != nil { t.Fatalf("SP27 执行失败: %v", err) } if out.Applied.Cents() != 12000 || out.Remaining.Cents() != 0 { t.Fatalf("applied/remaining=(%d,%d),期望 (12000,0)", out.Applied.Cents(), out.Remaining.Cents()) } if len(out.Items) != 2 { t.Fatalf("核销明细=%d 条,期望 2(100.00 + 20.00)", len(out.Items)) } // 顺序:最早课先核销 if out.Items[0].StudentCourseID != rows[0].ID || out.Items[0].Amount.Cents() != 10000 { t.Fatalf("首条核销=%+v,期望 CS-A 全额 100.00", out.Items[0]) } if out.Items[1].StudentCourseID != rows[1].ID || out.Items[1].Amount.Cents() != 2000 { t.Fatalf("次条核销=%+v,期望 CS-B 截到 20.00", out.Items[1]) } if got := cents(t, loadStudentCourse(t, db, rows[0].ID).NetReceipts); got != 10000 { t.Fatalf("CS-A net_receipts=%d 分,期望 10000", got) } if got := cents(t, loadStudentCourse(t, db, rows[0].ID).RemainderReceivable); got != 0 { t.Fatalf("CS-A remainder_receivable=%d 分,期望 0", got) } if got := cents(t, loadStudentCourse(t, db, rows[1].ID).RemainderReceivable); got != 3000 { t.Fatalf("CS-B remainder_receivable=%d 分,期望 3000(50-20)", got) } // 反向断言:第三条(晚课)没有被核销;CS-B 只因 1 条记录被核销(去重生效,未重复累加) if got := cents(t, loadStudentCourse(t, db, rows[2].ID).NetReceipts); got != 0 { t.Fatalf("CS-C 不应被核销,net_receipts=%d", got) } if got := cents(t, loadStudentCourse(t, db, rows[1].ID).NetReceipts); got != 2000 { t.Fatalf("CS-B net_receipts=%d 分,期望 2000(重复规则未导致重复核销)", got) } // money<=0 → no-op(dump: inputMoney<=0 直接 LEAVE) noop, err := svc.CounteractArrears(ctx, CounteractArrearsInput{ StudentID: student, InstitutionID: 1, CampusID: 10, Money: 0, }) if err != nil { t.Fatalf("money=0 应 no-op,实际: %v", err) } if noop.Applied.Cents() != 0 || len(noop.Items) != 0 { t.Fatalf("money=0 时不应核销,实际 %+v", noop) } // 参数校验:campus<=0 → 3140 _, err = svc.CounteractArrears(ctx, CounteractArrearsInput{StudentID: student, InstitutionID: 1, CampusID: 0, Money: 100}) requireAppCode(t, err, apperr.CodeBusinessParam) } // TestRebuildActivityUsedLogMapping SP30:DELETE + INSERT 重建;orderid = courses_studentid 映射不可改。 func TestRebuildActivityUsedLogMapping(t *testing.T) { db := newLearningDB(t) svc := NewMaintenanceService(db, superScope()) ctx := context.Background() pool := &model.CoursesActivityUserDiscount{ ActivityID: 5, InstitutionID: 1, CampusID: 10, UserID: "01HUSER00000000000000001", Income: "500.00", DiscountAmount: "100.00", DiscountAmountUsed: "20.00", } if err := db.Create(pool).Error; err != nil { t.Fatalf("seed 优惠池失败: %v", err) } businessTime := time.Date(2026, 3, 1, 10, 30, 0, 0, time.Local) logs := []*model.CoursesActivityUserDiscountLog{ {ActivityUserDiscountID: pool.ID, CoursesStudentID: int64Ptr(77), CoursesStudentNo: stringPtr("CS-UUID-1"), DiscountAmountUsed: "-20.00", CreateTime: businessTime, Memo: stringPtr("使用充值赠送金额")}, // 孤儿流水(池行不存在)→ 新模型 activity_id/user_id NOT NULL,无法表达 → 跳过 {ActivityUserDiscountID: 999, CoursesStudentNo: stringPtr("CS-UUID-2"), DiscountAmountUsed: "-5.00", CreateTime: businessTime}, } for _, row := range logs { if err := db.Create(row).Error; err != nil { t.Fatalf("seed 优惠流水失败: %v", err) } } // 旧 used_log 脏行(应被清空) if err := db.Create(&model.CoursesActivityUsedLog{ActivityID: 9, UserID: "01HOLD000000000000000001", ActivityMoneyUsed: "1.00"}).Error; err != nil { t.Fatalf("seed 旧 used_log 失败: %v", err) } out, err := svc.RebuildActivityUsedLog(ctx) if err != nil { t.Fatalf("SP30 执行失败: %v", err) } if out.Scanned != 2 || out.Deleted != 1 || out.Inserted != 1 || out.Skipped != 1 { t.Fatalf("scanned/deleted/inserted/skipped=(%d,%d,%d,%d),期望 (2,1,1,1)", out.Scanned, out.Deleted, out.Inserted, out.Skipped) } var rebuilt []model.CoursesActivityUsedLog if err := db.Find(&rebuilt).Error; err != nil { t.Fatalf("读取重建结果失败: %v", err) } if len(rebuilt) != 1 { t.Fatalf("重建后 used_log=%d 行,期望 1(旧行已清空 + 孤儿跳过)", len(rebuilt)) } row := rebuilt[0] if row.ActivityID != 5 || row.UserID != pool.UserID { t.Fatalf("重建行 activity/user=(%d,%s),期望 (5,%s)", row.ActivityID, row.UserID, pool.UserID) } // ★ 映射断言:order_no 取 courses_student_no(旧 UUID 单号),**不是** int64 id if row.OrderNo == nil || *row.OrderNo != "CS-UUID-1" { t.Fatalf("order_no=%v,期望 CS-UUID-1(orderid = courses_studentid 映射不可改)", row.OrderNo) } if got := cents(t, row.ActivityMoneyUsed); got != -2000 { t.Fatalf("activity_money_used=%d 分,期望 -2000(原值写入,不翻转符号)", got) } if !row.CreateTime.Equal(businessTime) { t.Fatalf("create_time=%v,期望 %v(业务时间逐值保留)", row.CreateTime, businessTime) } } // TestRebuildActivityUsedLogRollbackKeepsRows BUG#3 反向断言:中途失败必须**整体回滚**, // 旧 used_log 一行都不能少(dump 的 TRUNCATE 会隐式提交 → 数据全丢)。 func TestRebuildActivityUsedLogRollbackKeepsRows(t *testing.T) { db := newLearningDB(t) pool := &model.CoursesActivityUserDiscount{ ActivityID: 5, InstitutionID: 1, CampusID: 10, UserID: "01HUSER00000000000000002", Income: "100.00", DiscountAmount: "10.00", DiscountAmountUsed: "0.00", } if err := db.Create(pool).Error; err != nil { t.Fatalf("seed 优惠池失败: %v", err) } if err := db.Create(&model.CoursesActivityUserDiscountLog{ ActivityUserDiscountID: pool.ID, CoursesStudentNo: stringPtr("CS-UUID-9"), DiscountAmountUsed: "-10.00", }).Error; err != nil { t.Fatalf("seed 优惠流水失败: %v", err) } old := &model.CoursesActivityUsedLog{ActivityID: 9, UserID: "01HOLD000000000000000002", ActivityMoneyUsed: "3.00"} if err := db.Create(old).Error; err != nil { t.Fatalf("seed 旧 used_log 失败: %v", err) } // 注入失败:所有 used_log 的 INSERT 一律报错(模拟"清空后重建失败") if err := db.Callback().Create().Before("gorm:create").Register("academic:fail_used_log_insert", func(tx *gorm.DB) { if tx.Statement != nil && tx.Statement.Schema != nil && tx.Statement.Schema.Table == "t_courses_activity_used_log" { _ = tx.AddError(errors.New("injected insert failure")) } }); err != nil { t.Fatalf("注册注入失败回调出错: %v", err) } svc := NewMaintenanceService(db, superScope()) if _, err := svc.RebuildActivityUsedLog(context.Background()); err == nil { t.Fatal("注入失败后 SP30 应返回错误") } var rows []model.CoursesActivityUsedLog if err := db.Find(&rows).Error; err != nil { t.Fatalf("读取 used_log 失败: %v", err) } if len(rows) != 1 || rows[0].ActivityID != 9 { t.Fatalf("回滚后 used_log=%d 行(期望保留旧 1 行)—— DELETE+INSERT 必须同事务(BUG#3)", len(rows)) } if got := cents(t, rows[0].ActivityMoneyUsed); got != 300 { t.Fatalf("回滚后旧行活动金额=%d 分,期望 300", got) } } // TestReplayActivitySigned SP31:只回写 status>0 的报名;已取消(status=0)不复活(反向断言)。 func TestReplayActivitySigned(t *testing.T) { db := newLearningDB(t) svc := NewMaintenanceService(db, superScope()) ctx := context.Background() responseUser := "01HRESPONSE000000000001" responseTime := time.Date(2026, 3, 2, 9, 0, 0, 0, time.Local) responseMsg := "审核通过" req := &model.UserBalanceRequest{ BizNo: "BR-1", Type: model.BalanceRequestTypeRecharge, UserID: "01HUSER00000000000000003", Approved: model.BalanceRequestApprovedPassed, ResponseUserID: &responseUser, ResponseTime: &responseTime, ResponseMsg: &responseMsg, Money: "500.00", } if err := db.Create(req).Error; err != nil { t.Fatalf("seed 工单失败: %v", err) } pendingReq := &model.UserBalanceRequest{ BizNo: "BR-2", Type: model.BalanceRequestTypeRecharge, UserID: "01HUSER00000000000000004", Approved: model.BalanceRequestApprovedPending, Money: "100.00", } if err := db.Create(pendingReq).Error; err != nil { t.Fatalf("seed 未审核工单失败: %v", err) } activePool := &model.CoursesActivityUserDiscount{ ActivityID: 5, InstitutionID: 1, CampusID: 10, UserID: "01HUSER00000000000000003", BalanceRequestID: int64Ptr(req.ID), Income: "500.00", DiscountAmount: "100.00", DiscountAmountUsed: "20.00", } cancelledPool := &model.CoursesActivityUserDiscount{ ActivityID: 6, InstitutionID: 1, CampusID: 10, UserID: "01HUSER00000000000000004", BalanceRequestID: int64Ptr(req.ID), Income: "500.00", DiscountAmount: "100.00", DiscountAmountUsed: "20.00", } pendingPool := &model.CoursesActivityUserDiscount{ ActivityID: 7, InstitutionID: 1, CampusID: 10, UserID: "01HUSER00000000000000005", BalanceRequestID: int64Ptr(pendingReq.ID), Income: "100.00", DiscountAmount: "10.00", } for _, row := range []*model.CoursesActivityUserDiscount{activePool, cancelledPool, pendingPool} { if err := db.Create(row).Error; err != nil { t.Fatalf("seed 优惠池失败: %v", err) } } signedActive := &model.CoursesActivitySigned{ActivityID: 5, UserID: activePool.UserID, Status: model.ActivitySignedSigned} signedCancelled := &model.CoursesActivitySigned{ActivityID: 6, UserID: cancelledPool.UserID, Status: model.ActivitySignedCancelled} for _, row := range []*model.CoursesActivitySigned{signedActive, signedCancelled} { if err := db.Create(row).Error; err != nil { t.Fatalf("seed 报名失败: %v", err) } } out, err := svc.ReplayActivitySigned(ctx) if err != nil { t.Fatalf("SP31 执行失败: %v", err) } if out.Scanned != 2 { t.Fatalf("scanned=%d,期望 2(未审核工单的池行被 INNER JOIN 排除)", out.Scanned) } if out.Updated != 1 || out.SkippedCanceled != 1 { t.Fatalf("updated/skippedCanceled=(%d,%d),期望 (1,1)", out.Updated, out.SkippedCanceled) } var active model.CoursesActivitySigned if err := db.Where("id = ?", signedActive.ID).First(&active).Error; err != nil { t.Fatalf("读取报名失败: %v", err) } if cents(t, active.RechargeAmount) != 50000 { t.Fatalf("recharge_amount=%s,期望 500.00", active.RechargeAmount) } if cents(t, active.ActivityMoney) != 10000 { t.Fatalf("activity_money=%s,期望 100.00", active.ActivityMoney) } if cents(t, active.ActivityMoneyUsed) != 2000 { t.Fatalf("activity_money_used=%s,期望 20.00", active.ActivityMoneyUsed) } if active.RequestNo == nil || *active.RequestNo != "BR-1" { t.Fatalf("request_no=%v,期望 BR-1(工单 biz_no)", active.RequestNo) } if active.ResponseUserID == nil || *active.ResponseUserID != responseUser { t.Fatalf("response_user_id=%v,期望 %s", active.ResponseUserID, responseUser) } if active.ResponseTime == nil || !active.ResponseTime.Equal(responseTime) { t.Fatalf("response_time=%v,期望 %v", active.ResponseTime, responseTime) } // 反向断言:已取消报名(status=0)不被历史工单复活 var cancelled model.CoursesActivitySigned if err := db.Where("id = ?", signedCancelled.ID).First(&cancelled).Error; err != nil { t.Fatalf("读取已取消报名失败: %v", err) } if cancelled.Status != model.ActivitySignedCancelled { t.Fatalf("已取消报名 status=%d,期望 0", cancelled.Status) } if cents(t, cancelled.RechargeAmount) != 0 || cancelled.RequestNo != nil { t.Fatalf("已取消报名被回写:recharge_amount=%s request_no=%v", cancelled.RechargeAmount, cancelled.RequestNo) } }