package smoke_test import ( "bytes" "context" "database/sql" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "testing" "time" _ "github.com/go-sql-driver/mysql" "github.com/golang-jwt/jwt/v5" ) const ( bagGiftAppCode = "lalu" bagGiftJWTSecret = "dev-shared-secret" bagGiftSenderID = int64(920001) bagGiftTargetID = int64(920002) bagGiftID = "bag_smoke_gift" bagGiftResourceCode = "bag_smoke_gift_resource" bagGiftEntitlement = "bag-smoke-entitlement" bagGiftPrice = int64(10) bagGiftHeatValue = int64(10) bagGiftSendCount = int64(2) ) func TestBagGiftLocalHTTPFlow(t *testing.T) { if os.Getenv("BAG_GIFT_REAL_HTTP_FLOW_TEST") != "1" { t.Skip("set BAG_GIFT_REAL_HTTP_FLOW_TEST=1 to run the local bag gift HTTP flow") } ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() db, err := sql.Open("mysql", bagGiftEnvOr("BAG_GIFT_MYSQL_DSN", "hyapp:hyapp@tcp(127.0.0.1:23306)/?parseTime=true&charset=utf8mb4&loc=UTC&multiStatements=true")) if err != nil { t.Fatalf("open mysql failed: %v", err) } defer db.Close() if err := db.PingContext(ctx); err != nil { t.Fatalf("mysql ping failed: %v", err) } flow := &bagGiftHTTPFlow{ db: db, http: &http.Client{Timeout: 15 * time.Second}, gateway: strings.TrimRight(bagGiftEnvOr("BAG_GIFT_GATEWAY", "http://127.0.0.1:13000"), "/"), startMS: time.Now().UTC().UnixMilli(), } regionID, err := flow.lookupUSRegion(ctx) if err != nil { t.Fatalf("lookup region failed: %v", err) } if err := flow.cleanup(ctx); err != nil { t.Fatalf("cleanup failed: %v", err) } if err := flow.seedUsers(ctx, regionID); err != nil { t.Fatalf("seed users failed: %v", err) } if err := flow.seedBagGift(ctx); err != nil { t.Fatalf("seed bag gift failed: %v", err) } if err := flow.assertMyGiftResourceVisible(ctx); err != nil { t.Fatalf("my gift resource assertion failed: %v", err) } roomID, err := flow.createRoomAndJoin(ctx) if err != nil { t.Fatalf("create room and join failed: %v", err) } if err := flow.assertBagPanelVisible(ctx, roomID); err != nil { t.Fatalf("bag gift panel assertion failed: %v", err) } commandID := bagGiftCommandID("send") sendResp, err := flow.sendBagGift(ctx, roomID, commandID) if err != nil { t.Fatalf("send bag gift failed: %v", err) } if strings.TrimSpace(sendResp.BillingReceiptID) == "" { t.Fatalf("send response must include billing_receipt_id: %+v", sendResp) } if sendResp.RoomHeat < bagGiftPrice*bagGiftSendCount { t.Fatalf("room_heat must include bag gift value, got=%d want>=%d resp=%+v", sendResp.RoomHeat, bagGiftPrice*bagGiftSendCount, sendResp) } if err := flow.assertWalletFacts(ctx, commandID, sendResp.BillingReceiptID); err != nil { t.Fatalf("wallet facts mismatch: %v", err) } if err := flow.assertRoomFacts(ctx, roomID, commandID); err != nil { t.Fatalf("room facts mismatch: %v", err) } t.Logf("bag gift HTTP smoke passed: gateway=%s room_id=%s command_id=%s receipt=%s", flow.gateway, roomID, commandID, sendResp.BillingReceiptID) } type bagGiftHTTPFlow struct { db *sql.DB http *http.Client gateway string startMS int64 } type bagGiftEnvelope struct { Code string `json:"code"` Message string `json:"message"` RequestID string `json:"request_id"` Data json.RawMessage `json:"data"` } type bagGiftCreateRoomData struct { Room struct { RoomID string `json:"room_id"` } `json:"room"` } type bagGiftSendData struct { BillingReceiptID string `json:"billing_receipt_id"` RoomHeat int64 `json:"room_heat"` } func (f *bagGiftHTTPFlow) lookupUSRegion(ctx context.Context) (int64, error) { var regionID int64 err := f.db.QueryRowContext(ctx, ` SELECT r.region_id FROM hyapp_user.regions r JOIN hyapp_user.region_countries rc ON rc.app_code = r.app_code AND rc.region_id = r.region_id WHERE r.app_code = ? AND rc.country_code = 'US' AND r.status = 'active' AND rc.status = 'active' ORDER BY r.region_id LIMIT 1`, bagGiftAppCode).Scan(®ionID) if err != nil { return 0, fmt.Errorf("lookup US region: %w", err) } return regionID, nil } func (f *bagGiftHTTPFlow) cleanup(ctx context.Context) error { // 这个 smoke 使用固定用户和固定资源,运行前先把本用例自己产生的房间、钱包流水和权益恢复到确定状态。 // 业务行为仍然只通过 gateway HTTP 发起;这里的 SQL 只负责隔离测试数据,避免旧房间 owner 唯一约束和旧权益库存影响结果。 queries := []struct { sql string args []any }{ {`DELETE FROM hyapp_room.room_background_images WHERE app_code = ? AND room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}}, {`DELETE FROM hyapp_room.room_snapshots WHERE app_code = ? AND room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}}, {`DELETE FROM hyapp_room.room_command_log WHERE app_code = ? AND (command_id LIKE 'bag-gift-smoke:%' OR room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?)))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}}, {`DELETE FROM hyapp_room.room_outbox WHERE app_code = ? AND room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}}, {`DELETE FROM hyapp_room.room_user_gift_stats WHERE app_code = ? AND room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}}, {`DELETE FROM hyapp_room.room_user_presence WHERE app_code = ? AND (user_id IN (?, ?) OR room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?)))`, []any{bagGiftAppCode, bagGiftSenderID, bagGiftTargetID, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}}, {`DELETE FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?)`, []any{bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}}, {`DELETE FROM hyapp_wallet.wallet_entries WHERE app_code = ? AND transaction_id IN (SELECT transaction_id FROM hyapp_wallet.wallet_transactions WHERE app_code = ? AND command_id LIKE 'bag-gift-smoke:%')`, []any{bagGiftAppCode, bagGiftAppCode}}, {`DELETE FROM hyapp_wallet.wallet_outbox WHERE app_code = ? AND command_id LIKE 'bag-gift-smoke:%'`, []any{bagGiftAppCode}}, {`DELETE FROM hyapp_wallet.wallet_transactions WHERE app_code = ? AND command_id LIKE 'bag-gift-smoke:%'`, []any{bagGiftAppCode}}, } for _, query := range queries { if _, err := f.db.ExecContext(ctx, query.sql, query.args...); err != nil { return err } } return nil } func (f *bagGiftHTTPFlow) seedUsers(ctx context.Context, regionID int64) error { if _, err := f.db.ExecContext(ctx, ` DELETE FROM hyapp_user.user_display_user_ids WHERE app_code = ? AND (user_id IN (?, ?) OR display_user_id IN ('920001', '920002'))`, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID, ); err != nil { return err } nowMS := time.Now().UTC().UnixMilli() users := []struct { id int64 name string avatar string }{ {bagGiftSenderID, "Bag Smoke Sender", "https://cdn.example.com/bag-smoke-sender.png"}, {bagGiftTargetID, "Bag Smoke Target", "https://cdn.example.com/bag-smoke-target.png"}, } for _, user := range users { displayID := strconv.FormatInt(user.id, 10) if _, err := f.db.ExecContext(ctx, ` INSERT INTO hyapp_user.users ( app_code, user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind, username, gender, country, region_id, avatar, profile_completed, profile_completed_at_ms, onboarding_status, status, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, 'default', ?, 'unknown', 'US', ?, ?, 1, ?, 'completed', 'active', ?, ?) ON DUPLICATE KEY UPDATE current_display_user_id = VALUES(current_display_user_id), current_display_user_id_kind = VALUES(current_display_user_id_kind), username = VALUES(username), country = VALUES(country), region_id = VALUES(region_id), avatar = VALUES(avatar), profile_completed = VALUES(profile_completed), profile_completed_at_ms = VALUES(profile_completed_at_ms), onboarding_status = VALUES(onboarding_status), status = VALUES(status), updated_at_ms = VALUES(updated_at_ms)`, bagGiftAppCode, user.id, displayID, displayID, user.name, regionID, user.avatar, nowMS, nowMS, nowMS, ); err != nil { return err } if _, err := f.db.ExecContext(ctx, ` INSERT INTO hyapp_user.user_display_user_ids ( app_code, display_user_id, user_id, display_user_id_kind, status, assigned_at_ms, activated_at_ms, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, 'default', 'active', ?, ?, ?, ?)`, bagGiftAppCode, displayID, user.id, nowMS, nowMS, nowMS, nowMS, ); err != nil { return err } } return nil } func (f *bagGiftHTTPFlow) seedBagGift(ctx context.Context) error { nowMS := time.Now().UTC().UnixMilli() if _, err := f.db.ExecContext(ctx, ` INSERT INTO hyapp_wallet.resources ( app_code, resource_code, resource_type, name, status, grantable, manager_grant_enabled, grant_strategy, wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount, usage_scope_json, asset_url, preview_url, animation_url, metadata_json, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms ) VALUES (?, ?, 'gift', 'Bag Smoke Gift', 'active', TRUE, TRUE, 'increase_quantity', '', 0, 'coin', ?, 0, JSON_ARRAY('gift_panel'), 'https://cdn.example.com/bag-smoke-gift.png', 'https://cdn.example.com/bag-smoke-gift-preview.png', 'https://cdn.example.com/bag-smoke-gift-animation.svga', JSON_OBJECT(), 0, 0, 0, ?, ?) ON DUPLICATE KEY UPDATE resource_type = 'gift', name = VALUES(name), status = 'active', grantable = TRUE, manager_grant_enabled = TRUE, grant_strategy = 'increase_quantity', price_type = 'coin', coin_price = VALUES(coin_price), usage_scope_json = VALUES(usage_scope_json), asset_url = VALUES(asset_url), preview_url = VALUES(preview_url), animation_url = VALUES(animation_url), updated_at_ms = VALUES(updated_at_ms)`, bagGiftAppCode, bagGiftResourceCode, bagGiftPrice, nowMS, nowMS, ); err != nil { return err } var resourceID int64 if err := f.db.QueryRowContext(ctx, ` SELECT resource_id FROM hyapp_wallet.resources WHERE app_code = ? AND resource_code = ?`, bagGiftAppCode, bagGiftResourceCode, ).Scan(&resourceID); err != nil { return err } if _, err := f.db.ExecContext(ctx, ` INSERT INTO hyapp_wallet.gift_configs ( app_code, gift_id, resource_id, status, name, sort_order, presentation_json, gift_type_code, effective_from_ms, effective_to_ms, effect_types_json, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, 'active', 'Bag Smoke Gift', 1, CAST(? AS JSON), 'normal', 0, 0, JSON_ARRAY(), 0, 0, ?, ?) ON DUPLICATE KEY UPDATE resource_id = VALUES(resource_id), status = 'active', name = VALUES(name), sort_order = VALUES(sort_order), presentation_json = VALUES(presentation_json), gift_type_code = 'normal', effective_from_ms = 0, effective_to_ms = 0, effect_types_json = JSON_ARRAY(), updated_at_ms = VALUES(updated_at_ms)`, bagGiftAppCode, bagGiftID, resourceID, `{"preview_url":"https://cdn.example.com/bag-smoke-gift-preview.png","animation_url":"https://cdn.example.com/bag-smoke-gift-animation.svga"}`, nowMS, nowMS, ); err != nil { return err } if _, err := f.db.ExecContext(ctx, ` INSERT INTO hyapp_wallet.gift_config_regions (app_code, gift_id, region_id, created_at_ms, updated_at_ms) VALUES (?, ?, 0, ?, ?) ON DUPLICATE KEY UPDATE updated_at_ms = VALUES(updated_at_ms)`, bagGiftAppCode, bagGiftID, nowMS, nowMS, ); err != nil { return err } if _, err := f.db.ExecContext(ctx, ` INSERT INTO hyapp_wallet.wallet_gift_prices ( app_code, gift_id, price_version, status, charge_asset_type, coin_price, gift_point_amount, heat_value, effective_at_ms, created_at_ms, updated_at_ms ) VALUES (?, ?, 'bag-smoke-v1', 'active', 'COIN', ?, 0, ?, 0, ?, ?) ON DUPLICATE KEY UPDATE status = 'active', charge_asset_type = 'COIN', coin_price = VALUES(coin_price), gift_point_amount = 0, heat_value = VALUES(heat_value), effective_at_ms = 0, updated_at_ms = VALUES(updated_at_ms)`, bagGiftAppCode, bagGiftID, bagGiftPrice, bagGiftHeatValue, nowMS, nowMS, ); err != nil { return err } if _, err := f.db.ExecContext(ctx, ` INSERT INTO hyapp_wallet.wallet_accounts ( app_code, user_id, asset_type, available_amount, frozen_amount, version, created_at_ms, updated_at_ms ) VALUES (?, ?, 'COIN', 0, 0, 1, ?, ?) ON DUPLICATE KEY UPDATE available_amount = 0, frozen_amount = 0, version = version + 1, updated_at_ms = VALUES(updated_at_ms)`, bagGiftAppCode, bagGiftSenderID, nowMS, nowMS, ); err != nil { return err } if _, err := f.db.ExecContext(ctx, ` INSERT INTO hyapp_wallet.user_resource_entitlements ( app_code, entitlement_id, user_id, resource_id, status, quantity, remaining_quantity, effective_at_ms, expires_at_ms, source_grant_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, 'active', 3, 3, 0, 0, 'bag-smoke-grant', ?, ?) ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), resource_id = VALUES(resource_id), status = 'active', quantity = 3, remaining_quantity = 3, effective_at_ms = 0, expires_at_ms = 0, source_grant_id = VALUES(source_grant_id), updated_at_ms = VALUES(updated_at_ms)`, bagGiftAppCode, bagGiftEntitlement, bagGiftSenderID, resourceID, nowMS, nowMS, ); err != nil { return err } return nil } func (f *bagGiftHTTPFlow) createRoomAndJoin(ctx context.Context) (string, error) { var createData bagGiftCreateRoomData if err := f.request(ctx, http.MethodPost, "/api/v1/rooms/create", bagGiftSenderID, map[string]any{ "command_id": bagGiftCommandID("create-room"), "seat_count": 10, "mode": "voice", "room_name": "Bag Gift Smoke Room", "room_avatar": "https://cdn.example.com/bag-gift-smoke-room.png", "room_description": "bag gift smoke", }, &createData); err != nil { return "", err } if strings.TrimSpace(createData.Room.RoomID) == "" { return "", fmt.Errorf("create room response missing room.room_id") } for _, userID := range []int64{bagGiftSenderID, bagGiftTargetID} { if err := f.request(ctx, http.MethodPost, "/api/v1/rooms/join", userID, map[string]any{ "room_id": createData.Room.RoomID, "command_id": bagGiftCommandID(fmt.Sprintf("join-%d", userID)), "role": "audience", }, nil); err != nil { return "", fmt.Errorf("join user %d: %w", userID, err) } } return createData.Room.RoomID, nil } func (f *bagGiftHTTPFlow) assertMyGiftResourceVisible(ctx context.Context) error { var raw json.RawMessage if err := f.request(ctx, http.MethodGet, "/api/v1/users/me/resources?resource_type=gift", bagGiftSenderID, nil, &raw); err != nil { return err } body := string(raw) if !strings.Contains(body, bagGiftEntitlement) || !strings.Contains(body, bagGiftResourceCode) { return fmt.Errorf("my resources does not contain bag gift entitlement/resource data=%s", body) } return nil } func (f *bagGiftHTTPFlow) assertBagPanelVisible(ctx context.Context, roomID string) error { var raw json.RawMessage if err := f.request(ctx, http.MethodGet, "/api/v1/rooms/"+url.PathEscape(roomID)+"/gift-panel", bagGiftSenderID, nil, &raw); err != nil { return err } body := string(raw) if !strings.Contains(body, bagGiftID) { return fmt.Errorf("gift panel does not contain gift_id=%s data=%s", bagGiftID, body) } if !strings.Contains(body, bagGiftEntitlement) || !strings.Contains(body, `"source":"bag"`) { return fmt.Errorf("gift panel does not contain bag entitlement/source data=%s", body) } return nil } func (f *bagGiftHTTPFlow) sendBagGift(ctx context.Context, roomID string, commandID string) (bagGiftSendData, error) { var data bagGiftSendData err := f.request(ctx, http.MethodPost, "/api/v1/rooms/gift/send", bagGiftSenderID, map[string]any{ "room_id": roomID, "command_id": commandID, "target_type": "user", "target_user_ids": []int64{bagGiftTargetID}, "gift_id": bagGiftID, "gift_count": bagGiftSendCount, "source": "bag", "entitlement_id": bagGiftEntitlement, }, &data) return data, err } func (f *bagGiftHTTPFlow) assertWalletFacts(ctx context.Context, commandID string, billingReceiptID string) error { var remaining int64 if err := f.db.QueryRowContext(ctx, ` SELECT remaining_quantity FROM hyapp_wallet.user_resource_entitlements WHERE app_code = ? AND entitlement_id = ?`, bagGiftAppCode, bagGiftEntitlement, ).Scan(&remaining); err != nil { return err } if remaining != 1 { return fmt.Errorf("remaining_quantity=%d, want 1", remaining) } var transactionID, bizType, status, chargeSource, chargeAssetType, gotBillingReceiptID string var coinSpent, heatValue int64 if err := f.db.QueryRowContext(ctx, ` SELECT transaction_id, biz_type, status, COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_source')), ''), COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_asset_type')), ''), COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.billing_receipt_id')), ''), COALESCE(JSON_EXTRACT(metadata_json, '$.coin_spent'), 0), COALESCE(JSON_EXTRACT(metadata_json, '$.heat_value'), 0) FROM hyapp_wallet.wallet_transactions WHERE app_code = ? AND command_id = ?`, bagGiftAppCode, commandID, ).Scan(&transactionID, &bizType, &status, &chargeSource, &chargeAssetType, &gotBillingReceiptID, &coinSpent, &heatValue); err != nil { return err } // Bag 送礼只是不扣 COIN 账户库存,交易快照里的 coin_spent/heat_value 仍表示礼物原价总值; // 房间贡献、麦位热度和主播积分继续复用这份价值口径,不能因为来源是背包就归零。 if bizType != "gift_debit" || status != "succeeded" || chargeSource != "bag" || chargeAssetType != "BAG" || coinSpent != bagGiftPrice*bagGiftSendCount || heatValue != bagGiftPrice*bagGiftSendCount { return fmt.Errorf("wallet transaction mismatch biz_type=%s status=%s source=%s asset=%s coin_spent=%d heat=%d", bizType, status, chargeSource, chargeAssetType, coinSpent, heatValue) } if gotBillingReceiptID != billingReceiptID { return fmt.Errorf("billing receipt mismatch metadata=%s response=%s transaction_id=%s", gotBillingReceiptID, billingReceiptID, transactionID) } var entryCount int64 if err := f.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM hyapp_wallet.wallet_entries WHERE app_code = ? AND transaction_id = ?`, bagGiftAppCode, transactionID, ).Scan(&entryCount); err != nil { return err } if entryCount != 0 { return fmt.Errorf("bag send must not create coin wallet entries, got=%d", entryCount) } return nil } func (f *bagGiftHTTPFlow) assertRoomFacts(ctx context.Context, roomID string, commandID string) error { var commandCount int64 if err := f.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM hyapp_room.room_command_log WHERE app_code = ? AND room_id = ? AND command_id = ? AND command_type = 'send_gift'`, bagGiftAppCode, roomID, commandID, ).Scan(&commandCount); err != nil { return err } if commandCount != 1 { return fmt.Errorf("room_command_log count=%d, want 1", commandCount) } var outboxCount int64 if err := f.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM hyapp_room.room_outbox WHERE app_code = ? AND room_id = ? AND event_type = 'RoomGiftSent' AND created_at_ms >= ?`, bagGiftAppCode, roomID, f.startMS, ).Scan(&outboxCount); err != nil { return err } if outboxCount == 0 { return fmt.Errorf("missing RoomGiftSent outbox event") } return nil } func (f *bagGiftHTTPFlow) request(ctx context.Context, method string, path string, userID int64, body any, out any) error { var reader io.Reader if body != nil { raw, err := json.Marshal(body) if err != nil { return err } reader = bytes.NewReader(raw) } req, err := http.NewRequestWithContext(ctx, method, f.gateway+path, reader) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+bagGiftTokenForUser(userID)) req.Header.Set("X-App-Code", bagGiftAppCode) if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := f.http.Do(req) if err != nil { return err } defer resp.Body.Close() raw, _ := io.ReadAll(resp.Body) var envelope bagGiftEnvelope if err := json.Unmarshal(raw, &envelope); err != nil { return fmt.Errorf("%s %s returned non-envelope status=%d body=%s", method, path, resp.StatusCode, string(raw)) } if resp.StatusCode < 200 || resp.StatusCode >= 300 || envelope.Code != "OK" { return fmt.Errorf("%s %s failed status=%d code=%s message=%s body=%s", method, path, resp.StatusCode, envelope.Code, envelope.Message, string(raw)) } if out == nil { return nil } if rawOut, ok := out.(*json.RawMessage); ok { *rawOut = append((*rawOut)[:0], envelope.Data...) return nil } if err := json.Unmarshal(envelope.Data, out); err != nil { return fmt.Errorf("decode data for %s %s failed: %w data=%s", method, path, err, string(envelope.Data)) } return nil } func bagGiftTokenForUser(userID int64) string { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "user_id": strconv.FormatInt(userID, 10), "app_code": bagGiftAppCode, "sid": "bag-gift-smoke-" + strconv.FormatInt(userID, 10), "profile_completed": true, "onboarding_status": "completed", "exp": time.Now().Add(2 * time.Hour).Unix(), "iat": time.Now().Unix(), }) signed, err := token.SignedString([]byte(bagGiftJWTSecret)) if err != nil { panic(err) } return signed } func bagGiftCommandID(key string) string { return fmt.Sprintf("bag-gift-smoke:%s:%d", key, time.Now().UTC().UnixNano()) } func bagGiftEnvOr(key string, fallback string) string { if value := strings.TrimSpace(os.Getenv(key)); value != "" { return value } return fallback }