package financeorder import ( "context" "database/sql" "errors" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "hyapp-admin-server/internal/config" ) func TestLegacyCoinSellerRechargeWriterGrantUpdatesBalanceAndRunningWater(t *testing.T) { walletDB, walletMock, cleanup := newLegacyWriterSQLMock(t) defer cleanup() writer := NewMySQLLegacyCoinSellerRechargeWriter(config.LegacyCoinSellerRechargeSourceConfig{ AppCode: "aslan", SysOrigin: "ATYOU", RequestTimeout: 5 * time.Second, }, nil, walletDB) grantedAt := time.Date(2026, 7, 9, 9, 30, 0, 0, time.UTC) walletMock.ExpectBegin() expectLegacyBalanceLock(walletMock, "ATYOU", 10001). WillReturnRows(sqlmock.NewRows([]string{"id", "earn_points", "consumption_points"}).AddRow(int64(77), "5000", "1000")) walletMock.ExpectQuery(`(?s)SELECT id\s+FROM user_freight_balance_running_water\s+WHERE sys_origin = \? AND remark = \?\s+LIMIT 1`). WithArgs("ATYOU", "MIFA-001"). WillReturnError(sql.ErrNoRows) walletMock.ExpectExec(`(?s)UPDATE user_freight_balance\s+SET earn_points = COALESCE\(earn_points, 0\) \+ \?, update_user = \?, update_time = \?\s+WHERE id = \?`). WithArgs(int64(1200), int64(10001), grantedAt, int64(77)). WillReturnResult(sqlmock.NewResult(0, 1)) walletMock.ExpectExec(`(?s)INSERT INTO user_freight_balance_running_water`). WithArgs(sqlmock.AnyArg(), "ATYOU", int64(10001), int64(10001), nil, int64(1200), int64(5200), "12.34", "MIFAPAY-进货", "MIFA-001", int64(10001), int64(10001), grantedAt, grantedAt). WillReturnResult(sqlmock.NewResult(0, 1)) walletMock.ExpectCommit() result, err := writer.GrantCoinSellerRecharge(context.Background(), legacyRechargeGrantInput{ UserID: 10001, ExternalOrderNo: "MIFA-001", ProviderCode: "mifapay", USDAmount: "12.34", CoinAmount: 1200, GrantedAt: grantedAt, }) if err != nil { t.Fatalf("legacy grant failed: %v", err) } if !strings.HasPrefix(result.TransactionID, "legacy-freight-purchase-") || result.BalanceAfter != 5200 { t.Fatalf("legacy grant result mismatch: %+v", result) } if err := walletMock.ExpectationsWereMet(); err != nil { t.Fatalf("wallet sql expectations: %v", err) } } func TestLegacyCoinSellerRechargeWriterGrantRejectsDuplicateOrder(t *testing.T) { walletDB, walletMock, cleanup := newLegacyWriterSQLMock(t) defer cleanup() writer := NewMySQLLegacyCoinSellerRechargeWriter(config.LegacyCoinSellerRechargeSourceConfig{ AppCode: "yumi", SysOrigin: "LIKEI", RequestTimeout: 5 * time.Second, }, nil, walletDB) walletMock.ExpectBegin() expectLegacyBalanceLock(walletMock, "LIKEI", 20002). WillReturnRows(sqlmock.NewRows([]string{"id", "earn_points", "consumption_points"}).AddRow(int64(88), "7000", "0")) walletMock.ExpectQuery(`(?s)SELECT id\s+FROM user_freight_balance_running_water\s+WHERE sys_origin = \? AND remark = \?\s+LIMIT 1`). WithArgs("LIKEI", "V5-001"). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(int64(9001))) walletMock.ExpectRollback() _, err := writer.GrantCoinSellerRecharge(context.Background(), legacyRechargeGrantInput{ UserID: 20002, ExternalOrderNo: "V5-001", ProviderCode: "v5pay", USDAmount: "10.00", CoinAmount: 1000, GrantedAt: time.Date(2026, 7, 9, 10, 0, 0, 0, time.UTC), }) if !errors.Is(err, errLegacyCoinSellerOrderDuplicate) { t.Fatalf("expected duplicate order error, got %v", err) } if err := walletMock.ExpectationsWereMet(); err != nil { t.Fatalf("wallet sql expectations: %v", err) } } func TestLegacyCoinSellerRechargeWriterResolveCoinSellerByAccountRequiresDealer(t *testing.T) { appDB, appMock, cleanupApp := newLegacyWriterSQLMock(t) defer cleanupApp() walletDB, walletMock, cleanupWallet := newLegacyWriterSQLMock(t) defer cleanupWallet() writer := NewMySQLLegacyCoinSellerRechargeWriter(config.LegacyCoinSellerRechargeSourceConfig{ AppCode: "aslan", SysOrigin: "ATYOU", RequestTimeout: 5 * time.Second, }, appDB, walletDB) expectLegacyUserLookup(appMock, "ATYOU", "agent001"). WillReturnRows(sqlmock.NewRows([]string{"id", "account", "country_id"}).AddRow(int64(10001), "agent001", int64(63))) expectLegacyCoinSellerCheck(walletMock, "ATYOU", 10001). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(int64(77))) target, err := writer.ResolveCoinSeller(context.Background(), " agent001 ") if err != nil { t.Fatalf("resolve legacy coin seller failed: %v", err) } if target.UserID != 10001 || target.DisplayUserID != "agent001" || target.CountryID != 63 { t.Fatalf("legacy coin seller target mismatch: %+v", target) } if err := appMock.ExpectationsWereMet(); err != nil { t.Fatalf("app sql expectations: %v", err) } if err := walletMock.ExpectationsWereMet(); err != nil { t.Fatalf("wallet sql expectations: %v", err) } } func TestLegacyCoinSellerRechargeWriterResolveCoinSellerRejectsNonDealer(t *testing.T) { appDB, appMock, cleanupApp := newLegacyWriterSQLMock(t) defer cleanupApp() walletDB, walletMock, cleanupWallet := newLegacyWriterSQLMock(t) defer cleanupWallet() writer := NewMySQLLegacyCoinSellerRechargeWriter(config.LegacyCoinSellerRechargeSourceConfig{ AppCode: "yumi", SysOrigin: "LIKEI", RequestTimeout: 5 * time.Second, }, appDB, walletDB) expectLegacyUserLookup(appMock, "LIKEI", "visitor001"). WillReturnRows(sqlmock.NewRows([]string{"id", "account", "country_id"}).AddRow(int64(20002), "visitor001", int64(86))) expectLegacyCoinSellerCheck(walletMock, "LIKEI", 20002). WillReturnError(sql.ErrNoRows) _, err := writer.ResolveCoinSeller(context.Background(), "visitor001") if !errors.Is(err, errLegacyCoinSellerNotFound) { t.Fatalf("expected non-dealer legacy user to be rejected, got %v", err) } if err := appMock.ExpectationsWereMet(); err != nil { t.Fatalf("app sql expectations: %v", err) } if err := walletMock.ExpectationsWereMet(); err != nil { t.Fatalf("wallet sql expectations: %v", err) } } func expectLegacyBalanceLock(mock sqlmock.Sqlmock, sysOrigin string, userID int64) *sqlmock.ExpectedQuery { return mock.ExpectQuery(`(?s)SELECT id, CAST\(COALESCE\(earn_points, 0\) AS CHAR\), CAST\(COALESCE\(consumption_points, 0\) AS CHAR\)\s+FROM user_freight_balance\s+WHERE sys_origin = \? AND user_id = \?.*FOR UPDATE`). WithArgs(sysOrigin, userID) } func expectLegacyUserLookup(mock sqlmock.Sqlmock, sysOrigin string, keyword string) *sqlmock.ExpectedQuery { return mock.ExpectQuery(`(?s)SELECT id, account, COALESCE\(country_id, 0\)\s+FROM user_base_info\s+WHERE origin_sys = \? AND \(account = \? OR CAST\(id AS CHAR\) = \?\).*LIMIT 1`). WithArgs(sysOrigin, keyword, keyword) } func expectLegacyCoinSellerCheck(mock sqlmock.Sqlmock, sysOrigin string, userID int64) *sqlmock.ExpectedQuery { return mock.ExpectQuery(`(?s)SELECT id\s+FROM user_freight_balance\s+WHERE sys_origin = \? AND user_id = \? AND COALESCE\(is_close, 0\) = 0 AND \(COALESCE\(dealer, 0\) = 1 OR COALESCE\(super_dealer, 0\) = 1\)\s+LIMIT 1`). WithArgs(sysOrigin, userID) } func newLegacyWriterSQLMock(t *testing.T) (*sql.DB, sqlmock.Sqlmock, func()) { t.Helper() db, mock, err := sqlmock.New() if err != nil { t.Fatalf("new sqlmock: %v", err) } return db, mock, func() { _ = db.Close() } }