package financewithdrawal import ( "context" "errors" "testing" "time" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/activityclient" "hyapp-admin-server/internal/integration/walletclient" "hyapp-admin-server/internal/model" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/repository" activityv1 "hyapp.local/api/proto/activity/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "github.com/DATA-DOG/go-sqlmock" "gorm.io/driver/mysql" "gorm.io/gorm" ) func TestOperationsApprovalMovesToFinanceWithoutWalletOrUserNotice(t *testing.T) { svc, mock, wallet, activity, closeService := newWithdrawalAuditServiceTest(t) defer closeService() expectWithdrawalMoneyAccess(mock, 8, "lalu") expectWithdrawalReviewLock(mock, model.WithdrawalApplicationStatusPending, model.WithdrawalOperationsStatusPending) mock.ExpectExec("UPDATE `admin_user_withdrawal_applications` SET").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectCommit() dto, err := svc.ApproveOperationsApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{ UserID: 8, Username: "operations", Permissions: []string{permissionAuditOperationsWithdrawals}, }, 77, "资料无误", "request-ops-approve") if err != nil { t.Fatalf("approve operations withdrawal failed: %v", err) } if dto.Status != model.WithdrawalApplicationStatusPending || dto.OperationsStatus != model.WithdrawalOperationsStatusApproved || dto.OperationsReviewerName != "operations" { t.Fatalf("operations approval state mismatch: %+v", dto) } if wallet.settleSalary != nil || wallet.releaseSalary != nil || wallet.settlePoint != nil || wallet.releasePoint != nil || activity.calls != 0 { t.Fatalf("operations approval must not touch wallet or send final notice: wallet=%+v notice_calls=%d", wallet, activity.calls) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations mismatch: %v", err) } } func TestOperationsRejectionReleasesFrozenBalanceAndSendsFinalNotice(t *testing.T) { svc, mock, wallet, activity, closeService := newWithdrawalAuditServiceTest(t) defer closeService() expectWithdrawalMoneyAccess(mock, 8, "lalu") expectWithdrawalRejectionClaim(mock, model.WithdrawalOperationsStatusPending) expectWithdrawalRejectionFinalize(mock) dto, err := svc.RejectOperationsApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{ UserID: 8, Username: "operations", Permissions: []string{permissionAuditOperationsWithdrawals}, }, 77, "收款资料不符", "request-ops-reject") if err != nil { t.Fatalf("reject operations withdrawal failed: %v", err) } if wallet.releaseSalary == nil || wallet.releaseSalary.GetCommandId() != "salary-withdrawal:77:operations" || wallet.releaseSalary.GetSalaryUsdMinor() != 5000 { t.Fatalf("operations rejection release mismatch: %+v", wallet.releaseSalary) } if wallet.settleSalary != nil || wallet.settlePoint != nil || wallet.releasePoint != nil { t.Fatalf("operations rejection must only release matching salary asset: %+v", wallet) } if activity.calls != 1 || activity.last.GetProducerEventId() != "operations-withdrawal:77:rejected" || activity.last.GetProducerEventType() != "operations_withdrawal_rejected" { t.Fatalf("operations rejection final notice mismatch: calls=%d request=%+v", activity.calls, activity.last) } if dto.Status != model.WithdrawalApplicationStatusRejected || dto.OperationsStatus != model.WithdrawalOperationsStatusRejected || dto.OperationsAuditTransactionID != "salary-release-tx" { t.Fatalf("operations rejection state mismatch: %+v", dto) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations mismatch: %v", err) } } func TestOperationsRejectionNoticeFailureStaysRejectingAndRetryKeepsFirstAuditContext(t *testing.T) { svc, mock, wallet, activity, closeService := newWithdrawalAuditServiceTest(t) defer closeService() noticeFailure := errors.New("activity unavailable") activity.failures = []error{noticeFailure, nil} expectWithdrawalMoneyAccess(mock, 8, "lalu") expectWithdrawalRejectionClaim(mock, model.WithdrawalOperationsStatusPending) _, err := svc.RejectOperationsApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{ UserID: 8, Username: "first-operator", Permissions: []string{permissionAuditOperationsWithdrawals}, }, 77, "first rejection reason", "request-ops-reject-first") if !errors.Is(err, noticeFailure) { t.Fatalf("first rejection must surface notice failure, got %v", err) } // 第二个运营人员重试时只能沿用第一次 claim 快照;数据库 rejecting 行禁止改点通过,也不允许改写拒绝原因。 expectWithdrawalMoneyAccess(mock, 18, "lalu") expectWithdrawalRejectionRetryClaim(mock, 8, "first-operator", "first rejection reason") expectWithdrawalRejectionFinalizeWithContext(mock, 8, "first-operator", "first rejection reason") dto, err := svc.RejectOperationsApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{ UserID: 18, Username: "retry-operator", Permissions: []string{permissionAuditOperationsWithdrawals}, }, 77, "changed retry reason", "request-ops-reject-retry") if err != nil { t.Fatalf("retry rejecting withdrawal failed: %v", err) } if dto.OperationsStatus != model.WithdrawalOperationsStatusRejected || wallet.releaseSalary.GetOperatorUserId() != 8 || wallet.releaseSalary.GetReason() != "first rejection reason" { t.Fatalf("retry must preserve first claim context: dto=%+v wallet=%+v", dto, wallet.releaseSalary) } if activity.calls != 2 || activity.last == nil || activity.last.GetBody() != "Your withdrawal request has been rejected. Reason: first rejection reason" { t.Fatalf("retry notice must preserve first rejection reason: calls=%d request=%+v", activity.calls, activity.last) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations mismatch: %v", err) } } func TestOperationsApprovalRejectsDurableRejectingClaimBeforeWalletOrNotice(t *testing.T) { svc, mock, wallet, activity, closeService := newWithdrawalAuditServiceTest(t) defer closeService() expectWithdrawalMoneyAccess(mock, 18, "lalu") mock.ExpectBegin() mock.ExpectQuery("SELECT \\* FROM `admin_user_withdrawal_applications`.*FOR UPDATE"). WithArgs("lalu", uint(77), 1). WillReturnRows(withdrawalAuditRowsWithOperationsContext(model.WithdrawalApplicationStatusPending, model.WithdrawalOperationsStatusRejecting, 8, "first-operator", "first rejection reason")) mock.ExpectRollback() _, err := svc.ApproveOperationsApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{ UserID: 18, Username: "retry-operator", Permissions: []string{permissionAuditOperationsWithdrawals}, }, 77, "改为通过", "request-ops-approve-after-reject") if !errors.Is(err, repository.ErrWithdrawalApplicationStageNotReviewable) { t.Fatalf("rejecting claim must block operations approval, got %v", err) } if wallet.settleSalary != nil || wallet.releaseSalary != nil || wallet.settlePoint != nil || wallet.releasePoint != nil || activity.calls != 0 { t.Fatalf("rejecting gate must run before integrations: wallet=%+v notices=%d", wallet, activity.calls) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations mismatch: %v", err) } } func TestFinanceReviewRejectsOperationsPendingBeforeWalletOrNotice(t *testing.T) { svc, mock, wallet, activity, closeService := newWithdrawalAuditServiceTest(t) defer closeService() expectWithdrawalMoneyAccess(mock, 9, "lalu") mock.ExpectBegin() mock.ExpectQuery("SELECT \\* FROM `admin_user_withdrawal_applications`.*FOR UPDATE"). WithArgs("lalu", uint(77), 1). WillReturnRows(withdrawalAuditRows(model.WithdrawalApplicationStatusPending, model.WithdrawalOperationsStatusPending)) mock.ExpectRollback() _, err := svc.ApproveApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{ UserID: 9, Username: "finance", Permissions: []string{permissionAuditWithdrawalApplications}, }, 77, "已打款", "https://example.com/proof.png", "request-finance-approve") if !errors.Is(err, repository.ErrWithdrawalApplicationStageNotReviewable) { t.Fatalf("finance must reject operations-pending application, got %v", err) } if wallet.settleSalary != nil || wallet.releaseSalary != nil || wallet.settlePoint != nil || wallet.releasePoint != nil || activity.calls != 0 { t.Fatalf("stage gate must run before wallet or notice: wallet=%+v notice_calls=%d", wallet, activity.calls) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations mismatch: %v", err) } } func TestOperationsListRejectsExplicitAppOutsideMoneyScope(t *testing.T) { svc, mock, wallet, activity, closeService := newWithdrawalAuditServiceTest(t) defer closeService() expectWithdrawalMoneyAccess(mock, 8, "lalu") _, _, err := svc.ListOperationsApplications(shared.Actor{UserID: 8}, repository.WithdrawalApplicationListOptions{AppCode: "huwaa"}) if !errors.Is(err, errWithdrawalMoneyScopeForbidden) { t.Fatalf("out-of-scope list must be forbidden, got %v", err) } if wallet.settleSalary != nil || wallet.releaseSalary != nil || activity.calls != 0 { t.Fatalf("out-of-scope list must not call integrations: wallet=%+v notices=%d", wallet, activity.calls) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations mismatch: %v", err) } } func TestOperationsListWithoutAppRestrictsToMoneyScopeApps(t *testing.T) { svc, mock, _, _, closeService := newWithdrawalAuditServiceTest(t) defer closeService() expectWithdrawalMoneyAccess(mock, 8, "lalu", "fami") mock.ExpectQuery("SELECT count\\(\\*\\) FROM `admin_user_withdrawal_applications`.*app_code IN.*operations_status <> \\?"). WithArgs("fami", "lalu", model.WithdrawalOperationsStatusSkipped). WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) mock.ExpectQuery("SELECT \\* FROM `admin_user_withdrawal_applications`.*app_code IN.*operations_status <> \\?"). WithArgs("fami", "lalu", model.WithdrawalOperationsStatusSkipped, 20). WillReturnRows(sqlmock.NewRows([]string{"id"})) items, total, err := svc.ListOperationsApplications(shared.Actor{UserID: 8}, repository.WithdrawalApplicationListOptions{Page: 1, PageSize: 20}) if err != nil || total != 0 || len(items) != 0 { t.Fatalf("money-scoped operations list mismatch: total=%d items=%+v err=%v", total, items, err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations mismatch: %v", err) } } func TestFinanceAuditRejectsHeaderAppOutsideMoneyScopeBeforeWallet(t *testing.T) { svc, mock, wallet, activity, closeService := newWithdrawalAuditServiceTest(t) defer closeService() expectWithdrawalMoneyAccess(mock, 9, "fami") _, err := svc.ApproveApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{ UserID: 9, Username: "finance", Permissions: []string{permissionAuditWithdrawalApplications}, }, 77, "paid", "https://example.com/proof.png", "request-out-of-scope") if !errors.Is(err, errWithdrawalMoneyScopeForbidden) { t.Fatalf("out-of-scope audit must be forbidden, got %v", err) } if wallet.settleSalary != nil || wallet.releaseSalary != nil || wallet.settlePoint != nil || wallet.releasePoint != nil || activity.calls != 0 { t.Fatalf("money scope gate must run before row lock, wallet, or notice: wallet=%+v notices=%d", wallet, activity.calls) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations mismatch: %v", err) } } func TestWithdrawalAuditNotificationEventIDPreservesFinanceCompatibility(t *testing.T) { t.Parallel() if got := withdrawalAuditNotificationEventID(77, repository.WithdrawalApplicationReviewStageFinance, model.WithdrawalApplicationStatusApproved); got != "finance-withdrawal:77:approved" { t.Fatalf("finance event id = %q", got) } if got := withdrawalAuditNotificationEventID(77, repository.WithdrawalApplicationReviewStageOperations, model.WithdrawalApplicationStatusRejected); got != "operations-withdrawal:77:rejected" { t.Fatalf("operations event id = %q", got) } } func TestApplyWalletDecisionRoutesPointApprovalToPointSettlement(t *testing.T) { wallet := &fakeAuditWalletClient{} svc := &Service{wallet: wallet} transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusApproved, "audit:77:approved", "fami", 42001, "POINT", 1_000_000, 25_000, 975_000, 200_000, 250, shared.Actor{UserID: 90001}, "paid", 77) if err != nil { t.Fatalf("apply point approval failed: %v", err) } if transactionID != "point-settle-tx" { t.Fatalf("point approval transaction id mismatch: %s", transactionID) } if wallet.settlePoint == nil || wallet.settlePoint.GetCommandId() != "audit:77:approved" || wallet.settlePoint.GetAppCode() != "fami" || wallet.settlePoint.GetUserId() != 42001 || wallet.settlePoint.GetAssetType() != "POINT" || wallet.settlePoint.GetGrossPointAmount() != 1_000_000 || wallet.settlePoint.GetFeePointAmount() != 25_000 || wallet.settlePoint.GetNetPointAmount() != 975_000 || wallet.settlePoint.GetPointsPerUsd() != 200_000 || wallet.settlePoint.GetFeeBps() != 250 || wallet.settlePoint.GetOperatorUserId() != 90001 || wallet.settlePoint.GetWithdrawalApplicationId() != "77" { t.Fatalf("SettlePointWithdrawal payload mismatch: %+v", wallet.settlePoint) } if wallet.settleSalary != nil || wallet.releasePoint != nil || wallet.releaseSalary != nil { t.Fatalf("point approval must not call legacy salary or release RPCs: wallet=%+v", wallet) } } func TestApplyWalletDecisionRoutesPointRejectionToPointRelease(t *testing.T) { wallet := &fakeAuditWalletClient{} svc := &Service{wallet: wallet} transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusRejected, "audit:78:rejected", "huwaa", 42002, "COIN_SELLER_POINT", 2_000_000, 100_000, 1_900_000, 100_000, 500, shared.Actor{UserID: 90002}, "bad address", 78) if err != nil { t.Fatalf("apply point rejection failed: %v", err) } if transactionID != "point-release-tx" { t.Fatalf("point rejection transaction id mismatch: %s", transactionID) } if wallet.releasePoint == nil || wallet.releasePoint.GetCommandId() != "audit:78:rejected" || wallet.releasePoint.GetAppCode() != "huwaa" || wallet.releasePoint.GetUserId() != 42002 || wallet.releasePoint.GetAssetType() != "COIN_SELLER_POINT" || wallet.releasePoint.GetGrossPointAmount() != 2_000_000 || wallet.releasePoint.GetFeePointAmount() != 100_000 || wallet.releasePoint.GetNetPointAmount() != 1_900_000 || wallet.releasePoint.GetOperatorUserId() != 90002 || wallet.releasePoint.GetReason() != "bad address" || wallet.releasePoint.GetWithdrawalApplicationId() != "78" { t.Fatalf("ReleasePointWithdrawal payload mismatch: %+v", wallet.releasePoint) } if wallet.settlePoint != nil || wallet.settleSalary != nil || wallet.releaseSalary != nil { t.Fatalf("point rejection must not call legacy salary or settle RPCs: wallet=%+v", wallet) } } func TestApplyWalletDecisionKeepsLegacySalaryAssetsOnSalaryRPC(t *testing.T) { wallet := &fakeAuditWalletClient{} svc := &Service{wallet: wallet} transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusApproved, "audit:79:approved", "lalu", 42003, "HOST_SALARY_USD", 12_345, 0, 0, 0, 0, shared.Actor{UserID: 90003}, "", 79) if err != nil { t.Fatalf("apply salary approval failed: %v", err) } if transactionID != "salary-settle-tx" { t.Fatalf("salary approval transaction id mismatch: %s", transactionID) } if wallet.settleSalary == nil || wallet.settleSalary.GetAppCode() != "lalu" || wallet.settleSalary.GetUserId() != 42003 || wallet.settleSalary.GetSalaryAssetType() != "HOST_SALARY_USD" || wallet.settleSalary.GetSalaryUsdMinor() != 12_345 || wallet.settleSalary.GetWithdrawalApplicationId() != "79" { t.Fatalf("SettleSalaryWithdrawal payload mismatch: %+v", wallet.settleSalary) } if wallet.settlePoint != nil || wallet.releasePoint != nil { t.Fatalf("legacy salary asset must not call point RPCs: wallet=%+v", wallet) } } type fakeAuditWalletClient struct { walletclient.Client settlePoint *walletv1.SettlePointWithdrawalRequest releasePoint *walletv1.ReleasePointWithdrawalRequest settleSalary *walletv1.SettleSalaryWithdrawalRequest releaseSalary *walletv1.ReleaseSalaryWithdrawalRequest } func (f *fakeAuditWalletClient) SettlePointWithdrawal(_ context.Context, req *walletv1.SettlePointWithdrawalRequest) (*walletv1.SettlePointWithdrawalResponse, error) { f.settlePoint = req return &walletv1.SettlePointWithdrawalResponse{TransactionId: "point-settle-tx"}, nil } func (f *fakeAuditWalletClient) ReleasePointWithdrawal(_ context.Context, req *walletv1.ReleasePointWithdrawalRequest) (*walletv1.ReleasePointWithdrawalResponse, error) { f.releasePoint = req return &walletv1.ReleasePointWithdrawalResponse{TransactionId: "point-release-tx"}, nil } func (f *fakeAuditWalletClient) SettleSalaryWithdrawal(_ context.Context, req *walletv1.SettleSalaryWithdrawalRequest) (*walletv1.SettleSalaryWithdrawalResponse, error) { f.settleSalary = req return &walletv1.SettleSalaryWithdrawalResponse{TransactionId: "salary-settle-tx"}, nil } func (f *fakeAuditWalletClient) ReleaseSalaryWithdrawal(_ context.Context, req *walletv1.ReleaseSalaryWithdrawalRequest) (*walletv1.ReleaseSalaryWithdrawalResponse, error) { f.releaseSalary = req return &walletv1.ReleaseSalaryWithdrawalResponse{TransactionId: "salary-release-tx"}, nil } type fakeAuditActivityClient struct { activityclient.Client last *activityv1.CreateInboxMessageRequest calls int failures []error } func (f *fakeAuditActivityClient) CreateInboxMessage(_ context.Context, req *activityv1.CreateInboxMessageRequest) (*activityv1.CreateInboxMessageResponse, error) { f.last = req f.calls++ if index := f.calls - 1; index < len(f.failures) && f.failures[index] != nil { return nil, f.failures[index] } return &activityv1.CreateInboxMessageResponse{}, nil } func newWithdrawalAuditServiceTest(t *testing.T) (*Service, sqlmock.Sqlmock, *fakeAuditWalletClient, *fakeAuditActivityClient, func()) { t.Helper() sqlDB, mock, err := sqlmock.New() if err != nil { t.Fatalf("create sql mock failed: %v", err) } gormDB, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{}) if err != nil { _ = sqlDB.Close() t.Fatalf("create gorm db failed: %v", err) } wallet := &fakeAuditWalletClient{} activity := &fakeAuditActivityClient{} svc := &Service{ store: repository.New(gormDB), wallet: wallet, activity: activity, now: func() time.Time { return time.UnixMilli(1_700_000_300_000).UTC() }, } return svc, mock, wallet, activity, func() { _ = sqlDB.Close() } } func expectWithdrawalReviewLock(mock sqlmock.Sqlmock, status string, operationsStatus string) { mock.ExpectBegin() mock.ExpectQuery("SELECT \\* FROM `admin_user_withdrawal_applications`.*FOR UPDATE"). WithArgs("lalu", uint(77), 1). WillReturnRows(withdrawalAuditRows(status, operationsStatus)) } func expectWithdrawalRejectionClaim(mock sqlmock.Sqlmock, operationsStatus string) { mock.ExpectBegin() mock.ExpectQuery("SELECT \\* FROM `admin_user_withdrawal_applications`.*FOR UPDATE"). WithArgs("lalu", uint(77), 1). WillReturnRows(withdrawalAuditRows(model.WithdrawalApplicationStatusPending, operationsStatus)) if operationsStatus == model.WithdrawalOperationsStatusPending { mock.ExpectExec("UPDATE `admin_user_withdrawal_applications` SET").WillReturnResult(sqlmock.NewResult(0, 1)) } mock.ExpectCommit() } func expectWithdrawalRejectionRetryClaim(mock sqlmock.Sqlmock, reviewerID uint, reviewerName string, remark string) { mock.ExpectBegin() mock.ExpectQuery("SELECT \\* FROM `admin_user_withdrawal_applications`.*FOR UPDATE"). WithArgs("lalu", uint(77), 1). WillReturnRows(withdrawalAuditRowsWithOperationsContext(model.WithdrawalApplicationStatusPending, model.WithdrawalOperationsStatusRejecting, reviewerID, reviewerName, remark)) mock.ExpectCommit() } func expectWithdrawalRejectionFinalize(mock sqlmock.Sqlmock) { expectWithdrawalRejectionFinalizeWithContext(mock, 8, "operations", "收款资料不符") } func expectWithdrawalRejectionFinalizeWithContext(mock sqlmock.Sqlmock, reviewerID uint, reviewerName string, remark string) { mock.ExpectBegin() mock.ExpectQuery("SELECT \\* FROM `admin_user_withdrawal_applications`.*FOR UPDATE"). WithArgs("lalu", uint(77), 1). WillReturnRows(withdrawalAuditRowsWithOperationsContext(model.WithdrawalApplicationStatusPending, model.WithdrawalOperationsStatusRejecting, reviewerID, reviewerName, remark)) mock.ExpectExec("UPDATE `admin_user_withdrawal_applications` SET").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectCommit() } func expectWithdrawalMoneyAccess(mock sqlmock.Sqlmock, userID uint, appCodes ...string) { mock.ExpectQuery("SELECT \\* FROM `admin_users` WHERE `admin_users`.`id` = \\? ORDER BY `admin_users`.`id` LIMIT \\?"). WillReturnRows(sqlmock.NewRows([]string{"id", "username", "name", "status"}).AddRow(userID, "reviewer", "Reviewer", "active")) mock.ExpectQuery("SELECT \\* FROM `admin_user_roles` WHERE `admin_user_roles`.`user_id` = \\?"). WillReturnRows(sqlmock.NewRows([]string{"user_id", "role_id"})) rows := sqlmock.NewRows([]string{"id", "user_id", "app_code", "region_id", "created_at_ms", "updated_at_ms"}) for index, appCode := range appCodes { rows.AddRow(index+1, userID, appCode, 0, int64(1_700_000_000_000), int64(1_700_000_000_000)) } mock.ExpectQuery("SELECT \\* FROM `admin_user_money_scopes` WHERE user_id = \\? ORDER BY app_code ASC, region_id ASC"). WillReturnRows(rows) } func withdrawalAuditRows(status string, operationsStatus string) *sqlmock.Rows { return withdrawalAuditRowsWithOperationsContext(status, operationsStatus, 0, "", "") } func withdrawalAuditRowsWithOperationsContext(status string, operationsStatus string, reviewerID uint, reviewerName string, remark string) *sqlmock.Rows { var storedReviewerID any var reviewedAtMS any operationsCommandID := "" if reviewerID > 0 { storedReviewerID = reviewerID reviewedAtMS = int64(1_700_000_300_000) operationsCommandID = "salary-withdrawal:77:operations" } return sqlmock.NewRows([]string{ "id", "app_code", "user_id", "salary_asset_type", "withdraw_amount", "withdraw_amount_minor", "withdraw_method", "withdraw_address", "freeze_command_id", "freeze_transaction_id", "audit_command_id", "audit_transaction_id", "status", "operations_status", "approver_user_id", "approver_name", "audit_remark", "audit_image_url", "approved_at_ms", "operations_reviewer_user_id", "operations_reviewer_name", "operations_audit_remark", "operations_audit_command_id", "operations_audit_transaction_id", "operations_reviewed_at_ms", "created_at_ms", "updated_at_ms", }).AddRow( 77, "lalu", "42001", "HOST_SALARY_USD", "50.00", int64(5000), "usdt_trc20", "TRON-address", "freeze-command", "freeze-tx", "", "", status, operationsStatus, nil, "", "", "", nil, storedReviewerID, reviewerName, remark, operationsCommandID, "", reviewedAtMS, int64(1_700_000_000_000), int64(1_700_000_000_000), ) }