313 lines
14 KiB
Go
313 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
|
userv1 "hyapp.local/api/proto/user/v1"
|
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
|
"hyapp/pkg/appcode"
|
|
domain "hyapp/services/activity-service/internal/domain/inviteactivity"
|
|
inviteactivityservice "hyapp/services/activity-service/internal/service/inviteactivity"
|
|
mysqlstorage "hyapp/services/activity-service/internal/storage/mysql"
|
|
)
|
|
|
|
const (
|
|
appCode = "lalu"
|
|
operatorAdminID = int64(900100100)
|
|
tierStatus = "active"
|
|
)
|
|
|
|
type walletEntrySummary struct {
|
|
Count int64 `json:"count"`
|
|
Coins int64 `json:"coins"`
|
|
}
|
|
|
|
func main() {
|
|
var (
|
|
activityAddr = flag.String("activity_addr", "127.0.0.1:14006", "activity-service gRPC address")
|
|
walletAddr = flag.String("wallet_addr", "127.0.0.1:13004", "wallet-service gRPC address")
|
|
activityDSN = flag.String("activity_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=UTC", "activity MySQL DSN")
|
|
walletDSN = flag.String("wallet_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=UTC", "wallet MySQL DSN")
|
|
)
|
|
flag.Parse()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
ctx = appcode.WithContext(ctx, appCode)
|
|
|
|
conn, err := grpc.DialContext(ctx, *activityAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
must("dial activity-service", err)
|
|
defer conn.Close()
|
|
adminClient := activityv1.NewAdminInviteActivityRewardServiceClient(conn)
|
|
rewardClient := activityv1.NewInviteActivityRewardServiceClient(conn)
|
|
walletConn, err := grpc.DialContext(ctx, *walletAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
must("dial wallet-service", err)
|
|
defer walletConn.Close()
|
|
walletClient := walletv1.NewWalletServiceClient(walletConn)
|
|
|
|
activityRepo, err := mysqlstorage.Open(ctx, *activityDSN)
|
|
must("open activity mysql", err)
|
|
defer activityRepo.Close()
|
|
must("migrate activity mysql", activityRepo.Migrate(ctx))
|
|
walletDB, err := sql.Open("mysql", *walletDSN)
|
|
must("open wallet mysql", err)
|
|
defer walletDB.Close()
|
|
must("ping wallet mysql", walletDB.PingContext(ctx))
|
|
|
|
runID := fmt.Sprintf("iar_%d", time.Now().UnixMilli())
|
|
inviterUserID := int64(880000000000) + time.Now().Unix()%10000000
|
|
invitedUserID := inviterUserID + 9001
|
|
now := time.Now().UTC()
|
|
cycle := inviteactivityservice.CycleForTime(now)
|
|
|
|
config := updateConfig(ctx, adminClient, runID)
|
|
rechargeTier := findTier(config.GetTiers(), domain.RewardTypeRecharge, runID+"_recharge_160k")
|
|
validTier := findTier(config.GetTiers(), domain.RewardTypeValidInvite, runID+"_valid_1")
|
|
|
|
service := inviteactivityservice.New(activityRepo, walletClient, fakeInviteAttributionSource{inviterUserID: inviterUserID})
|
|
consumeInvite(ctx, service, runID, inviterUserID, invitedUserID, now)
|
|
afterInviteInviterStatus := getStatus(ctx, rewardClient, inviterUserID)
|
|
afterInviteInviteeStatus := getStatus(ctx, rewardClient, invitedUserID)
|
|
inviterInviteClaim := findClaim(afterInviteInviterStatus.GetClaims(), domain.RewardTypeInviteInviter)
|
|
inviteeInviteClaim := findClaim(afterInviteInviteeStatus.GetClaims(), domain.RewardTypeInvitee)
|
|
assert(inviterInviteClaim.GetRewardCoinAmount() == 700, "inviter per invite reward should be 700 coins")
|
|
assert(inviteeInviteClaim.GetRewardCoinAmount() == 200, "invitee per invite reward should be 200 coins")
|
|
consumeRecharge(ctx, service, runID, inviterUserID, 1, "google_play", 80000, now.Add(time.Minute))
|
|
consumeRecharge(ctx, service, runID, inviterUserID, 2, "mifapay", 40000, now.Add(2*time.Minute))
|
|
consumeRecharge(ctx, service, runID, inviterUserID, 3, "coin_seller_transfer", 40000, now.Add(3*time.Minute))
|
|
consumeValidInvite(ctx, service, runID, inviterUserID, 1, now.Add(4*time.Minute))
|
|
|
|
beforeClaim := getStatus(ctx, rewardClient, inviterUserID)
|
|
assert(beforeClaim.GetProgress().GetTotalRechargeCoinAmount() == 160000, "invite recharge progress should be 160000 coins")
|
|
assert(beforeClaim.GetProgress().GetValidInviteCount() == 1, "valid invite progress should be 1")
|
|
leaderboard := listLeaderboard(ctx, rewardClient)
|
|
assert(len(leaderboard.GetEntries()) > 0, "leaderboard should include at least one entry")
|
|
leaderboardEntry := findLeaderboardEntry(leaderboard.GetEntries(), inviterUserID)
|
|
assert(leaderboardEntry != nil, "leaderboard should include inviter")
|
|
assert(leaderboardEntry.GetValidInviteCount() == 1, "leaderboard valid invite count should be 1")
|
|
|
|
rechargeClaim := claim(ctx, rewardClient, inviterUserID, domain.RewardTypeRecharge, rechargeTier.GetTierId(), runID+"_claim_recharge")
|
|
validClaim := claim(ctx, rewardClient, inviterUserID, domain.RewardTypeValidInvite, validTier.GetTierId(), runID+"_claim_valid")
|
|
afterClaim := getStatus(ctx, rewardClient, inviterUserID)
|
|
assert(afterClaim.GetProgress().GetClaimedRewardCoinAmount() == 3500, "claimed reward progress should be 3500 coins")
|
|
|
|
walletInviteInviter := queryWalletEntry(ctx, walletDB, inviterInviteClaim.GetWalletTransactionId())
|
|
walletInvitee := queryWalletEntry(ctx, walletDB, inviteeInviteClaim.GetWalletTransactionId())
|
|
walletRecharge := queryWalletEntry(ctx, walletDB, rechargeClaim.GetWalletTransactionId())
|
|
walletValid := queryWalletEntry(ctx, walletDB, validClaim.GetWalletTransactionId())
|
|
assert(walletInviteInviter.Coins == 700, "wallet inviter per invite reward should credit 700 coins")
|
|
assert(walletInvitee.Coins == 200, "wallet invitee per invite reward should credit 200 coins")
|
|
assert(walletRecharge.Coins == 2500, "wallet recharge tier reward should credit 2500 coins")
|
|
assert(walletValid.Coins == 300, "wallet valid invite reward should credit 300 coins")
|
|
|
|
out := map[string]any{
|
|
"run_id": runID,
|
|
"inviter_user_id": inviterUserID,
|
|
"invited_user_id": invitedUserID,
|
|
"cycle": cycle.Key,
|
|
"progress_before_claim": map[string]int64{
|
|
"total_recharge_coin_amount": beforeClaim.GetProgress().GetTotalRechargeCoinAmount(),
|
|
"valid_invite_count": beforeClaim.GetProgress().GetValidInviteCount(),
|
|
},
|
|
"leaderboard_entry": map[string]any{
|
|
"rank_no": leaderboardEntry.GetRankNo(),
|
|
"user_id": leaderboardEntry.GetUserId(),
|
|
"valid_invite_count": leaderboardEntry.GetValidInviteCount(),
|
|
"total_recharge_coin_amount": leaderboardEntry.GetTotalRechargeCoinAmount(),
|
|
},
|
|
"claims": []map[string]any{
|
|
{"reward_type": inviterInviteClaim.GetRewardType(), "tier_code": inviterInviteClaim.GetTierCode(), "reward_coin_amount": inviterInviteClaim.GetRewardCoinAmount(), "wallet_transaction_id": inviterInviteClaim.GetWalletTransactionId()},
|
|
{"reward_type": inviteeInviteClaim.GetRewardType(), "tier_code": inviteeInviteClaim.GetTierCode(), "reward_coin_amount": inviteeInviteClaim.GetRewardCoinAmount(), "wallet_transaction_id": inviteeInviteClaim.GetWalletTransactionId()},
|
|
{"reward_type": rechargeClaim.GetRewardType(), "tier_code": rechargeClaim.GetTierCode(), "reward_coin_amount": rechargeClaim.GetRewardCoinAmount(), "wallet_transaction_id": rechargeClaim.GetWalletTransactionId()},
|
|
{"reward_type": validClaim.GetRewardType(), "tier_code": validClaim.GetTierCode(), "reward_coin_amount": validClaim.GetRewardCoinAmount(), "wallet_transaction_id": validClaim.GetWalletTransactionId()},
|
|
},
|
|
"wallet_entries": map[string]walletEntrySummary{
|
|
"invite_inviter": walletInviteInviter,
|
|
"invite_invitee": walletInvitee,
|
|
"recharge": walletRecharge,
|
|
"valid_invite": walletValid,
|
|
},
|
|
"progress_after_claim": map[string]int64{
|
|
"claimed_reward_coin_amount": afterClaim.GetProgress().GetClaimedRewardCoinAmount(),
|
|
},
|
|
}
|
|
encoded, _ := json.MarshalIndent(out, "", " ")
|
|
fmt.Println(string(encoded))
|
|
}
|
|
|
|
func updateConfig(ctx context.Context, client activityv1.AdminInviteActivityRewardServiceClient, runID string) *activityv1.InviteActivityRewardConfig {
|
|
resp, err := client.UpdateInviteActivityRewardConfig(ctx, &activityv1.UpdateInviteActivityRewardConfigRequest{
|
|
Meta: meta(runID + "_update_config"),
|
|
Enabled: true,
|
|
PerInviteInviterRewardCoinAmount: 700,
|
|
PerInviteInviteeRewardCoinAmount: 200,
|
|
OperatorAdminId: operatorAdminID,
|
|
Tiers: []*activityv1.InviteActivityRewardTier{
|
|
{RewardType: domain.RewardTypeRecharge, TierCode: runID + "_recharge_80k", TierName: "Local recharge 80k", ThresholdCoinAmount: 80000, RewardCoinAmount: 1000, Status: tierStatus, SortOrder: 1},
|
|
{RewardType: domain.RewardTypeRecharge, TierCode: runID + "_recharge_160k", TierName: "Local recharge 160k", ThresholdCoinAmount: 160000, RewardCoinAmount: 2500, Status: tierStatus, SortOrder: 2},
|
|
{RewardType: domain.RewardTypeValidInvite, TierCode: runID + "_valid_1", TierName: "Local valid invite 1", ThresholdValidInviteCount: 1, RewardCoinAmount: 300, Status: tierStatus, SortOrder: 1},
|
|
{RewardType: domain.RewardTypeValidInvite, TierCode: runID + "_valid_2", TierName: "Local valid invite 2", ThresholdValidInviteCount: 2, RewardCoinAmount: 900, Status: tierStatus, SortOrder: 2},
|
|
},
|
|
})
|
|
must("update invite activity config", err)
|
|
return resp.GetConfig()
|
|
}
|
|
|
|
func consumeInvite(ctx context.Context, service *inviteactivityservice.Service, runID string, inviterUserID int64, invitedUserID int64, occurredAt time.Time) {
|
|
err := service.ConsumeInviteEvent(ctx, domain.InviteEvent{
|
|
AppCode: appCode,
|
|
EventID: runID + "_invite_bound_1",
|
|
EventType: domain.EventTypeUserInvited,
|
|
InvitedUserID: invitedUserID,
|
|
InviterUserID: inviterUserID,
|
|
InviteCode: runID,
|
|
Source: "invite_h5",
|
|
OccurredAtMS: occurredAt.UnixMilli(),
|
|
})
|
|
must("consume invite bound event", err)
|
|
}
|
|
|
|
func consumeRecharge(ctx context.Context, service *inviteactivityservice.Service, runID string, inviterUserID int64, index int64, rechargeType string, coins int64, occurredAt time.Time) {
|
|
err := service.ConsumeWalletRechargeEvent(ctx, domain.RechargeEvent{
|
|
AppCode: appCode,
|
|
EventID: fmt.Sprintf("%s_recharge_%d", runID, index),
|
|
EventType: domain.EventTypeWalletRechargeRecorded,
|
|
TransactionID: fmt.Sprintf("%s_tx_%d", runID, index),
|
|
CommandID: fmt.Sprintf("%s_cmd_%d", runID, index),
|
|
InvitedUserID: inviterUserID + 100 + index,
|
|
InviterUserID: inviterUserID,
|
|
RechargeCoinAmount: coins,
|
|
RechargeType: rechargeType,
|
|
OccurredAtMS: occurredAt.UnixMilli(),
|
|
})
|
|
must("consume invite recharge "+rechargeType, err)
|
|
}
|
|
|
|
func consumeValidInvite(ctx context.Context, service *inviteactivityservice.Service, runID string, inviterUserID int64, index int64, occurredAt time.Time) {
|
|
err := service.ConsumeValidInviteEvent(ctx, domain.ValidInviteEvent{
|
|
AppCode: appCode,
|
|
EventID: fmt.Sprintf("%s_valid_%d", runID, index),
|
|
EventType: domain.EventTypeUserInviteBecameValid,
|
|
InvitedUserID: inviterUserID + 200 + index,
|
|
InviterUserID: inviterUserID,
|
|
OccurredAtMS: occurredAt.UnixMilli(),
|
|
})
|
|
must("consume valid invite", err)
|
|
}
|
|
|
|
func getStatus(ctx context.Context, client activityv1.InviteActivityRewardServiceClient, userID int64) *activityv1.InviteActivityRewardStatus {
|
|
resp, err := client.GetInviteActivityRewardStatus(ctx, &activityv1.GetInviteActivityRewardStatusRequest{
|
|
Meta: meta("get_status"),
|
|
UserId: userID,
|
|
})
|
|
must("get invite activity status", err)
|
|
return resp.GetStatus()
|
|
}
|
|
|
|
func listLeaderboard(ctx context.Context, client activityv1.InviteActivityRewardServiceClient) *activityv1.ListInviteActivityLeaderboardResponse {
|
|
resp, err := client.ListInviteActivityLeaderboard(ctx, &activityv1.ListInviteActivityLeaderboardRequest{
|
|
Meta: meta("list_leaderboard"),
|
|
Page: 1,
|
|
PageSize: 50,
|
|
})
|
|
must("list invite activity leaderboard", err)
|
|
return resp
|
|
}
|
|
|
|
func claim(ctx context.Context, client activityv1.InviteActivityRewardServiceClient, userID int64, rewardType string, tierID int64, commandID string) *activityv1.InviteActivityRewardClaim {
|
|
resp, err := client.ClaimInviteActivityReward(ctx, &activityv1.ClaimInviteActivityRewardRequest{
|
|
Meta: meta(commandID),
|
|
UserId: userID,
|
|
RewardType: rewardType,
|
|
TierId: tierID,
|
|
CommandId: commandID,
|
|
})
|
|
must("claim invite activity reward "+rewardType, err)
|
|
return resp.GetClaim()
|
|
}
|
|
|
|
func queryWalletEntry(ctx context.Context, db *sql.DB, transactionID string) walletEntrySummary {
|
|
var summary walletEntrySummary
|
|
err := db.QueryRowContext(ctx, `
|
|
SELECT COUNT(*), COALESCE(SUM(available_delta), 0)
|
|
FROM wallet_entries
|
|
WHERE app_code = ? AND transaction_id = ? AND asset_type = 'COIN'`,
|
|
appCode, transactionID,
|
|
).Scan(&summary.Count, &summary.Coins)
|
|
must("query wallet entry "+transactionID, err)
|
|
return summary
|
|
}
|
|
|
|
func findTier(tiers []*activityv1.InviteActivityRewardTier, rewardType string, tierCode string) *activityv1.InviteActivityRewardTier {
|
|
for _, tier := range tiers {
|
|
if tier.GetRewardType() == rewardType && tier.GetTierCode() == tierCode {
|
|
return tier
|
|
}
|
|
}
|
|
log.Fatalf("tier not found: %s %s", rewardType, tierCode)
|
|
return nil
|
|
}
|
|
|
|
func findClaim(claims []*activityv1.InviteActivityRewardClaim, rewardType string) *activityv1.InviteActivityRewardClaim {
|
|
for _, claim := range claims {
|
|
if claim.GetRewardType() == rewardType {
|
|
return claim
|
|
}
|
|
}
|
|
log.Fatalf("claim not found: %s", rewardType)
|
|
return nil
|
|
}
|
|
|
|
func findLeaderboardEntry(entries []*activityv1.InviteActivityLeaderboardEntry, userID int64) *activityv1.InviteActivityLeaderboardEntry {
|
|
for _, entry := range entries {
|
|
if entry.GetUserId() == userID {
|
|
return entry
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func meta(requestID string) *activityv1.RequestMeta {
|
|
return &activityv1.RequestMeta{
|
|
RequestId: requestID,
|
|
Caller: "invite-activity-local-flow",
|
|
AppCode: appCode,
|
|
SentAtMs: time.Now().UTC().UnixMilli(),
|
|
}
|
|
}
|
|
|
|
func assert(ok bool, message string) {
|
|
if !ok {
|
|
log.Fatal(message)
|
|
}
|
|
}
|
|
|
|
func must(step string, err error) {
|
|
if err != nil {
|
|
log.Fatalf("%s: %v", step, err)
|
|
}
|
|
}
|
|
|
|
type fakeInviteAttributionSource struct {
|
|
inviterUserID int64
|
|
}
|
|
|
|
func (s fakeInviteAttributionSource) GetInviteAttribution(_ context.Context, invitedUserID int64) (*userv1.InviteAttribution, error) {
|
|
return &userv1.InviteAttribution{
|
|
Found: true,
|
|
AppCode: appCode,
|
|
InvitedUserId: invitedUserID,
|
|
InviterUserId: s.inviterUserID,
|
|
}, nil
|
|
}
|