礼物布局
This commit is contained in:
parent
5d2c39f8db
commit
7f16956b7d
@ -95,6 +95,10 @@ func (h *Handler) CreateResource(c *gin.Context) {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := validateResourceMetadata(req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := h.wallet.CreateResource(c.Request.Context(), req.createProto(c))
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
@ -203,6 +207,10 @@ func (h *Handler) UpdateResource(c *gin.Context) {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := validateResourceMetadata(req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
protoReq := req.updateProto(c, resourceID)
|
||||
resp, err := h.wallet.UpdateResource(c.Request.Context(), protoReq)
|
||||
if err != nil {
|
||||
|
||||
@ -2,6 +2,7 @@ package resource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@ -150,6 +151,131 @@ func TestProfileCardResourceMetadataKeepsLayoutOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceMetadataKeepsMP4AlphaLayoutForAnimatedResources(t *testing.T) {
|
||||
for _, resourceType := range []string{"gift", "vehicle", "avatar_frame", "chat_bubble", "floating_screen", "mic_seat_animation"} {
|
||||
metadata := resourceMetadataJSON(resourceType, "", mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", true), "", "")
|
||||
payload := resourceMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||
t.Fatalf("%s mp4 metadata json mismatch: %v", resourceType, err)
|
||||
}
|
||||
if payload.AnimationFormat != resourceAnimationFormatMP4 || payload.MP4AlphaLayout == nil {
|
||||
t.Fatalf("%s mp4 metadata should be kept: %s", resourceType, metadata)
|
||||
}
|
||||
if payload.MP4AlphaLayout.AlphaLayout != mp4AlphaLayoutAlphaLeftRGBRight || payload.MP4AlphaLayout.RGBFrame[0] != 750 {
|
||||
t.Fatalf("%s mp4 layout mismatch: %+v", resourceType, payload.MP4AlphaLayout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceMetadataKeepsNormalMP4Layout(t *testing.T) {
|
||||
metadata := resourceMetadataJSON("gift", "", `{
|
||||
"animation_format": "mp4",
|
||||
"mp4_alpha_layout": {
|
||||
"alpha_layout": "normal",
|
||||
"video_w": 750,
|
||||
"video_h": 1334,
|
||||
"rgb_frame": [1, 2, 3, 4],
|
||||
"alpha_frame": null,
|
||||
"confirmed": true,
|
||||
"detect_version": 1
|
||||
}
|
||||
}`, "", "")
|
||||
payload := resourceMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||
t.Fatalf("normal mp4 metadata json mismatch: %v", err)
|
||||
}
|
||||
if payload.MP4AlphaLayout == nil || payload.MP4AlphaLayout.AlphaLayout != mp4AlphaLayoutNormal {
|
||||
t.Fatalf("normal mp4 metadata should be kept: %s", metadata)
|
||||
}
|
||||
if payload.MP4AlphaLayout.AlphaFrame != nil || payload.MP4AlphaLayout.RGBFrame[2] != 750 || payload.MP4AlphaLayout.RGBFrame[3] != 1334 {
|
||||
t.Fatalf("normal mp4 layout should be normalized to full frame: %+v", payload.MP4AlphaLayout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileCardResourceMetadataKeepsLayoutAndMP4AlphaLayout(t *testing.T) {
|
||||
metadata := resourceMetadataJSON(resourceTypeProfileCard, "", `{
|
||||
"profile_card_layout": {
|
||||
"source_width": 1136,
|
||||
"source_height": 1680,
|
||||
"color_content_width": 750,
|
||||
"content_top": 117,
|
||||
"content_bottom": 1666,
|
||||
"content_height": 1550,
|
||||
"content_top_ratio": 0,
|
||||
"content_height_ratio": 0,
|
||||
"detect_version": 99
|
||||
},
|
||||
"mp4_alpha_layout": {
|
||||
"alpha_layout": "rgb_left_alpha_right",
|
||||
"video_w": 1472,
|
||||
"video_h": 1328,
|
||||
"rgb_frame": [0, 0, 736, 1328],
|
||||
"alpha_frame": [736, 0, 736, 1328],
|
||||
"confirmed": true,
|
||||
"detect_version": 1
|
||||
}
|
||||
}`, "", "")
|
||||
payload := resourceMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||
t.Fatalf("profile card mp4 metadata json mismatch: %v", err)
|
||||
}
|
||||
if payload.ProfileCardLayout == nil || payload.ProfileCardLayout.ContentTop != 117 {
|
||||
t.Fatalf("profile card layout should be kept: %+v", payload.ProfileCardLayout)
|
||||
}
|
||||
if payload.MP4AlphaLayout == nil || payload.MP4AlphaLayout.AlphaLayout != mp4AlphaLayoutRGBLeftAlphaRight {
|
||||
t.Fatalf("profile card mp4 layout should be kept: %+v", payload.MP4AlphaLayout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateResourceMetadataRejectsInvalidMP4AlphaLayout(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
req resourceRequest
|
||||
}{
|
||||
{
|
||||
name: "unconfirmed",
|
||||
req: resourceRequest{
|
||||
ResourceType: "gift",
|
||||
MetadataJSON: mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad rect",
|
||||
req: resourceRequest{
|
||||
ResourceType: "gift",
|
||||
MetadataJSON: `{
|
||||
"mp4_alpha_layout": {
|
||||
"alpha_layout": "alpha_left_rgb_right",
|
||||
"video_w": 1500,
|
||||
"video_h": 1334,
|
||||
"rgb_frame": [750, 0, 800, 1334],
|
||||
"alpha_frame": [0, 0, 750, 1334],
|
||||
"confirmed": true,
|
||||
"detect_version": 1
|
||||
}
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported type",
|
||||
req: resourceRequest{
|
||||
ResourceType: resourceTypeBadge,
|
||||
MetadataJSON: mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", true),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := validateResourceMetadata(tc.req); err == nil {
|
||||
t.Fatalf("invalid mp4 metadata should be rejected")
|
||||
}
|
||||
})
|
||||
}
|
||||
if err := validateResourceMetadata(resourceRequest{ResourceType: "gift", MetadataJSON: mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", true)}); err != nil {
|
||||
t.Fatalf("valid mp4 metadata rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileCardResourceMetadataRejectsInvalidLayout(t *testing.T) {
|
||||
metadata := resourceMetadataJSON(resourceTypeProfileCard, "", `{
|
||||
"profile_card_layout": {
|
||||
@ -182,3 +308,19 @@ func TestValidateBadgeResourceRequiresBadgeForm(t *testing.T) {
|
||||
t.Fatalf("non-badge resource should not require badge form: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mp4AlphaLayoutMetadataJSON(alphaLayout string, confirmed bool) string {
|
||||
return `{
|
||||
"animation_format": "mp4",
|
||||
"mp4_alpha_layout": {
|
||||
"alpha_layout": "` + alphaLayout + `",
|
||||
"video_w": 1500,
|
||||
"video_h": 1334,
|
||||
"rgb_frame": [750, 0, 750, 1334],
|
||||
"alpha_frame": [0, 0, 750, 1334],
|
||||
"confirmed": ` + strconv.FormatBool(confirmed) + `,
|
||||
"detect_version": 1
|
||||
},
|
||||
"debug": true
|
||||
}`
|
||||
}
|
||||
|
||||
@ -24,6 +24,20 @@ const (
|
||||
resourceTypeProfileCard = "profile_card"
|
||||
)
|
||||
|
||||
const (
|
||||
resourceAnimationFormatMP4 = "mp4"
|
||||
)
|
||||
|
||||
const (
|
||||
mp4AlphaLayoutNormal = "normal"
|
||||
mp4AlphaLayoutAlphaLeftRGBRight = "alpha_left_rgb_right"
|
||||
mp4AlphaLayoutRGBLeftAlphaRight = "rgb_left_alpha_right"
|
||||
mp4AlphaLayoutAlphaTopRGBBottom = "alpha_top_rgb_bottom"
|
||||
mp4AlphaLayoutRGBTopAlphaBottom = "rgb_top_alpha_bottom"
|
||||
mp4AlphaLayoutCustom = "custom"
|
||||
mp4AlphaLayoutDetectVersion int64 = 1
|
||||
)
|
||||
|
||||
const (
|
||||
resourcePriceTypeCoin = "coin"
|
||||
resourcePriceTypeFree = "free"
|
||||
@ -88,6 +102,23 @@ type badgeMetadataPayload struct {
|
||||
DefaultSlot string `json:"default_slot,omitempty"`
|
||||
}
|
||||
|
||||
type resourceMetadataPayload struct {
|
||||
AnimationFormat string `json:"animation_format,omitempty"`
|
||||
MP4AlphaLayout *mp4AlphaLayoutMetadataPayload `json:"mp4_alpha_layout,omitempty"`
|
||||
ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
|
||||
}
|
||||
|
||||
type mp4AlphaLayoutMetadataPayload struct {
|
||||
AlphaLayout string `json:"alpha_layout"`
|
||||
VideoWidth int64 `json:"video_w"`
|
||||
VideoHeight int64 `json:"video_h"`
|
||||
RGBFrame []int64 `json:"rgb_frame"`
|
||||
AlphaFrame []int64 `json:"alpha_frame"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Confidence float64 `json:"confidence,omitempty"`
|
||||
DetectVersion int64 `json:"detect_version"`
|
||||
}
|
||||
|
||||
type profileCardMetadataPayload struct {
|
||||
ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
|
||||
}
|
||||
@ -559,6 +590,39 @@ func validateResourceBadgeForm(req resourceRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateResourceMetadata(req resourceRequest) error {
|
||||
resourceType := normalizeResourceType(req.ResourceType)
|
||||
payload, fields, err := parseResourceMetadataPayload(req.MetadataJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("资源元数据格式不正确")
|
||||
}
|
||||
if payload == nil {
|
||||
return nil
|
||||
}
|
||||
if rawFormat, ok := fields["animation_format"]; ok && len(rawFormat) > 0 {
|
||||
animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat))
|
||||
if animationFormat != "" && animationFormat != resourceAnimationFormatMP4 {
|
||||
return fmt.Errorf("资源动效格式不支持")
|
||||
}
|
||||
}
|
||||
if _, ok := fields["mp4_alpha_layout"]; !ok {
|
||||
return nil
|
||||
}
|
||||
if !resourceAllowsAnimationMetadata(resourceType) {
|
||||
return fmt.Errorf("当前资源类型不支持 MP4 透明布局")
|
||||
}
|
||||
layout, err := sanitizeMP4AlphaLayoutMetadata(payload.MP4AlphaLayout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// MP4 透明布局是客户端合成协议,后台只能保存人工确认后的显式矩形,
|
||||
// 否则 Flutter 会重新落回不稳定的自动识别路径,黑场或弱 mask 素材仍会展示错误。
|
||||
if !layout.Confirmed {
|
||||
return fmt.Errorf("请确认 MP4 透明布局")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceGrantStrategy(resourceType string) string {
|
||||
switch resourceType {
|
||||
case resourceTypeCoin:
|
||||
@ -573,14 +637,25 @@ func resourceGrantStrategy(resourceType string) string {
|
||||
}
|
||||
|
||||
func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON string, badgeKind string, levelTrack string) string {
|
||||
switch resourceType {
|
||||
case resourceTypeBadge:
|
||||
if resourceType == resourceTypeBadge {
|
||||
return badgeMetadataJSON(badgeForm, badgeKind, levelTrack)
|
||||
case resourceTypeProfileCard:
|
||||
return profileCardMetadataJSON(metadataJSON)
|
||||
default:
|
||||
}
|
||||
|
||||
rawPayload, _, err := parseResourceMetadataPayload(metadataJSON)
|
||||
if err != nil || rawPayload == nil {
|
||||
return "{}"
|
||||
}
|
||||
payload := resourceMetadataPayload{}
|
||||
if resourceType == resourceTypeProfileCard {
|
||||
payload.ProfileCardLayout = sanitizeProfileCardLayoutMetadata(rawPayload.ProfileCardLayout)
|
||||
}
|
||||
if resourceAllowsAnimationMetadata(resourceType) {
|
||||
if layout, err := sanitizeMP4AlphaLayoutMetadata(rawPayload.MP4AlphaLayout); err == nil && layout != nil && layout.Confirmed {
|
||||
payload.AnimationFormat = resourceAnimationFormatMP4
|
||||
payload.MP4AlphaLayout = layout
|
||||
}
|
||||
}
|
||||
return marshalResourceMetadataPayload(payload)
|
||||
}
|
||||
|
||||
func badgeMetadataJSON(badgeForm string, badgeKind string, levelTrack string) string {
|
||||
@ -611,7 +686,7 @@ func profileCardMetadataJSON(metadataJSON string) string {
|
||||
if raw == "" {
|
||||
return "{}"
|
||||
}
|
||||
payload := profileCardMetadataPayload{}
|
||||
payload := resourceMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||
return "{}"
|
||||
}
|
||||
@ -627,6 +702,128 @@ func profileCardMetadataJSON(metadataJSON string) string {
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func parseResourceMetadataPayload(metadataJSON string) (*resourceMetadataPayload, map[string]json.RawMessage, error) {
|
||||
raw := strings.TrimSpace(metadataJSON)
|
||||
if raw == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
fields := map[string]json.RawMessage{}
|
||||
if err := json.Unmarshal([]byte(raw), &fields); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
payload := resourceMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &payload, fields, nil
|
||||
}
|
||||
|
||||
func marshalResourceMetadataPayload(payload resourceMetadataPayload) string {
|
||||
if payload.AnimationFormat == "" && payload.MP4AlphaLayout == nil && payload.ProfileCardLayout == nil {
|
||||
return "{}"
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func sanitizeMP4AlphaLayoutMetadata(layout *mp4AlphaLayoutMetadataPayload) (*mp4AlphaLayoutMetadataPayload, error) {
|
||||
if layout == nil {
|
||||
return nil, fmt.Errorf("MP4 透明布局参数不完整")
|
||||
}
|
||||
videoWidth := layout.VideoWidth
|
||||
videoHeight := layout.VideoHeight
|
||||
if videoWidth <= 0 || videoHeight <= 0 {
|
||||
return nil, fmt.Errorf("MP4 视频宽高不正确")
|
||||
}
|
||||
alphaLayout := normalizeMP4AlphaLayout(layout.AlphaLayout)
|
||||
if alphaLayout == "" {
|
||||
return nil, fmt.Errorf("MP4 透明布局类型不支持")
|
||||
}
|
||||
detectVersion := layout.DetectVersion
|
||||
if detectVersion <= 0 {
|
||||
detectVersion = mp4AlphaLayoutDetectVersion
|
||||
}
|
||||
confidence := layout.Confidence
|
||||
if confidence < 0 {
|
||||
confidence = 0
|
||||
}
|
||||
if confidence > 1 {
|
||||
confidence = 1
|
||||
}
|
||||
|
||||
if alphaLayout == mp4AlphaLayoutNormal {
|
||||
// normal 也是显式布局:RGB 使用完整视频帧,alpha_frame 固定为 null,
|
||||
// 这样 native 层能明确跳过 split-alpha 猜测,普通 MP4 不再被误判。
|
||||
return &mp4AlphaLayoutMetadataPayload{
|
||||
AlphaLayout: mp4AlphaLayoutNormal,
|
||||
VideoWidth: videoWidth,
|
||||
VideoHeight: videoHeight,
|
||||
RGBFrame: []int64{0, 0, videoWidth, videoHeight},
|
||||
AlphaFrame: nil,
|
||||
Confirmed: layout.Confirmed,
|
||||
Confidence: confidence,
|
||||
DetectVersion: detectVersion,
|
||||
}, nil
|
||||
}
|
||||
if !validMP4Frame(layout.RGBFrame, videoWidth, videoHeight) || !validMP4Frame(layout.AlphaFrame, videoWidth, videoHeight) {
|
||||
return nil, fmt.Errorf("MP4 透明布局矩形不正确")
|
||||
}
|
||||
// 透明 MP4 的 RGB 与 alpha 矩形直接影响客户端 shader 取样,后端只做边界归一化,
|
||||
// 不把字段重算成左右或上下默认值,避免覆盖前端人工确认过的 custom 细节。
|
||||
return &mp4AlphaLayoutMetadataPayload{
|
||||
AlphaLayout: alphaLayout,
|
||||
VideoWidth: videoWidth,
|
||||
VideoHeight: videoHeight,
|
||||
RGBFrame: append([]int64(nil), layout.RGBFrame[:4]...),
|
||||
AlphaFrame: append([]int64(nil), layout.AlphaFrame[:4]...),
|
||||
Confirmed: layout.Confirmed,
|
||||
Confidence: confidence,
|
||||
DetectVersion: detectVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validMP4Frame(frame []int64, videoWidth int64, videoHeight int64) bool {
|
||||
if len(frame) < 4 {
|
||||
return false
|
||||
}
|
||||
left, top, width, height := frame[0], frame[1], frame[2], frame[3]
|
||||
if left < 0 || top < 0 || width <= 0 || height <= 0 {
|
||||
return false
|
||||
}
|
||||
return left+width <= videoWidth && top+height <= videoHeight
|
||||
}
|
||||
|
||||
func normalizeMP4AlphaLayout(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case mp4AlphaLayoutNormal:
|
||||
return mp4AlphaLayoutNormal
|
||||
case mp4AlphaLayoutAlphaLeftRGBRight:
|
||||
return mp4AlphaLayoutAlphaLeftRGBRight
|
||||
case mp4AlphaLayoutRGBLeftAlphaRight:
|
||||
return mp4AlphaLayoutRGBLeftAlphaRight
|
||||
case mp4AlphaLayoutAlphaTopRGBBottom:
|
||||
return mp4AlphaLayoutAlphaTopRGBBottom
|
||||
case mp4AlphaLayoutRGBTopAlphaBottom:
|
||||
return mp4AlphaLayoutRGBTopAlphaBottom
|
||||
case mp4AlphaLayoutCustom:
|
||||
return mp4AlphaLayoutCustom
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func resourceAllowsAnimationMetadata(resourceType string) bool {
|
||||
switch resourceType {
|
||||
case "gift", "vehicle", "avatar_frame", "chat_bubble", "floating_screen", resourceTypeProfileCard:
|
||||
return true
|
||||
default:
|
||||
return strings.HasPrefix(resourceType, "mic_seat_")
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeProfileCardLayoutMetadata(layout *profileCardLayoutMetadataPayload) *profileCardLayoutMetadataPayload {
|
||||
if layout == nil {
|
||||
return nil
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
package mysql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||
"hyapp/services/wallet-service/internal/testutil/mysqltest"
|
||||
)
|
||||
|
||||
func TestDebitCPBreakupFeeDebitsCoinAndReplaysIdempotently(t *testing.T) {
|
||||
repo := mysqltest.NewRepository(t)
|
||||
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
||||
repo.SetBalance(7001, 1000)
|
||||
|
||||
command := ledger.CPBreakupFeeCommand{
|
||||
AppCode: appcode.Default,
|
||||
CommandID: "cp_break_fee_1",
|
||||
UserID: 7001,
|
||||
RelationshipID: "cp_rel:7001:7002",
|
||||
RelationType: "cp",
|
||||
Amount: 88,
|
||||
}
|
||||
first, err := repo.DebitCPBreakupFee(ctx, command)
|
||||
if err != nil {
|
||||
t.Fatalf("debit cp breakup fee failed: %v", err)
|
||||
}
|
||||
if first.CoinSpent != 88 || first.CoinBalanceAfter != 912 || first.Balance.AvailableAmount != 912 {
|
||||
t.Fatalf("receipt mismatch: %+v", first)
|
||||
}
|
||||
if repo.CountRows("wallet_transactions", "command_id = ?", command.CommandID) != 1 {
|
||||
t.Fatalf("expected one wallet transaction")
|
||||
}
|
||||
if repo.CountRows("wallet_entries", "transaction_id = ?", first.TransactionID) != 1 {
|
||||
t.Fatalf("expected one wallet entry")
|
||||
}
|
||||
|
||||
second, err := repo.DebitCPBreakupFee(ctx, command)
|
||||
if err != nil {
|
||||
t.Fatalf("debit cp breakup fee replay failed: %v", err)
|
||||
}
|
||||
if second.TransactionID != first.TransactionID || second.CoinBalanceAfter != first.CoinBalanceAfter {
|
||||
t.Fatalf("idempotent replay mismatch: first=%+v second=%+v", first, second)
|
||||
}
|
||||
if repo.CountRows("wallet_entries", "transaction_id = ?", first.TransactionID) != 1 {
|
||||
t.Fatalf("idempotent replay must not create another entry")
|
||||
}
|
||||
|
||||
command.Amount = 89
|
||||
if _, err := repo.DebitCPBreakupFee(ctx, command); xerr.CodeOf(err) != xerr.LedgerConflict {
|
||||
t.Fatalf("same command with different amount must conflict, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebitCPBreakupFeeRejectsInsufficientBalance(t *testing.T) {
|
||||
repo := mysqltest.NewRepository(t)
|
||||
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
||||
repo.SetBalance(7101, 10)
|
||||
|
||||
_, err := repo.DebitCPBreakupFee(ctx, ledger.CPBreakupFeeCommand{
|
||||
AppCode: appcode.Default,
|
||||
CommandID: "cp_break_fee_insufficient",
|
||||
UserID: 7101,
|
||||
RelationshipID: "cp_rel:7101:7102",
|
||||
RelationType: "sister",
|
||||
Amount: 20,
|
||||
})
|
||||
if xerr.CodeOf(err) != xerr.InsufficientBalance {
|
||||
t.Fatalf("insufficient balance must be rejected, got %v", err)
|
||||
}
|
||||
if repo.CountRows("wallet_transactions", "command_id = ?", "cp_break_fee_insufficient") != 0 {
|
||||
t.Fatalf("insufficient balance must not create transaction")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user