feat: add media supply billing center
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -97,6 +98,40 @@ type MediaSupplyWalletStatus struct {
|
||||
TenantName *string `json:"tenant_name,omitempty"`
|
||||
}
|
||||
|
||||
type MediaSupplyBillingEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
TenantName *string `json:"tenant_name,omitempty"`
|
||||
UserID int64 `json:"user_id"`
|
||||
UserName *string `json:"user_name,omitempty"`
|
||||
UserPhone *string `json:"user_phone,omitempty"`
|
||||
OrderID *int64 `json:"order_id,omitempty"`
|
||||
OrderTitle *string `json:"order_title,omitempty"`
|
||||
OrderStatus *string `json:"order_status,omitempty"`
|
||||
ExternalOrderCode *string `json:"external_order_code,omitempty"`
|
||||
DeltaCents int64 `json:"delta_cents"`
|
||||
BalanceAfterCents int64 `json:"balance_after_cents"`
|
||||
Reason string `json:"reason"`
|
||||
Note *string `json:"note,omitempty"`
|
||||
SalesCents int64 `json:"sales_cents"`
|
||||
CostCents int64 `json:"cost_cents"`
|
||||
GrossProfitCents int64 `json:"gross_profit_cents"`
|
||||
CreatedBy *int64 `json:"created_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type MediaSupplyBillingSummary struct {
|
||||
TotalEntries int64 `json:"total_entries"`
|
||||
TotalSalesCents int64 `json:"total_sales_cents"`
|
||||
TotalCostCents int64 `json:"total_cost_cents"`
|
||||
GrossProfitCents int64 `json:"gross_profit_cents"`
|
||||
NetWalletDeltaCents int64 `json:"net_wallet_delta_cents"`
|
||||
OrderDebitCents int64 `json:"order_debit_cents"`
|
||||
OrderRefundCents int64 `json:"order_refund_cents"`
|
||||
RechargeCents int64 `json:"recharge_cents"`
|
||||
ManualAdjustmentCents int64 `json:"manual_adjustment_cents"`
|
||||
}
|
||||
|
||||
type ListMediaSupplyWalletsInput struct {
|
||||
Keyword string
|
||||
TenantID int64
|
||||
@@ -111,6 +146,26 @@ type ListMediaSupplyWalletsResult struct {
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
type ListMediaSupplyBillingsInput struct {
|
||||
Keyword string
|
||||
TenantID int64
|
||||
UserID int64
|
||||
OrderID int64
|
||||
Reason string
|
||||
StartAt *time.Time
|
||||
EndAt *time.Time
|
||||
Page int
|
||||
Size int
|
||||
}
|
||||
|
||||
type ListMediaSupplyBillingsResult struct {
|
||||
Items []MediaSupplyBillingEntry `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Summary MediaSupplyBillingSummary `json:"summary"`
|
||||
}
|
||||
|
||||
type AdjustMediaSupplyWalletInput struct {
|
||||
TenantID int64
|
||||
UserID int64
|
||||
@@ -371,6 +426,187 @@ func (s *MediaSupplyService) ListWallets(ctx context.Context, input ListMediaSup
|
||||
return &ListMediaSupplyWalletsResult{Items: items, Total: total, Page: page, Size: size}, nil
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) ListBillings(ctx context.Context, input ListMediaSupplyBillingsInput) (*ListMediaSupplyBillingsResult, error) {
|
||||
if s == nil || s.pool == nil {
|
||||
return nil, response.ErrServiceUnavailable(50360, "media_supply_store_unavailable", "媒体投稿服务暂不可用")
|
||||
}
|
||||
page, size := normalizeOpsMediaSupplyPagination(input.Page, input.Size)
|
||||
args := make([]any, 0, 8)
|
||||
where := []string{"1=1"}
|
||||
needsSearchJoin := false
|
||||
if input.TenantID > 0 {
|
||||
args = append(args, input.TenantID)
|
||||
where = append(where, fmt.Sprintf("l.tenant_id = $%d", len(args)))
|
||||
}
|
||||
if input.UserID > 0 {
|
||||
args = append(args, input.UserID)
|
||||
where = append(where, fmt.Sprintf("l.user_id = $%d", len(args)))
|
||||
}
|
||||
if input.OrderID > 0 {
|
||||
args = append(args, input.OrderID)
|
||||
where = append(where, fmt.Sprintf("l.order_id = $%d", len(args)))
|
||||
}
|
||||
if reason := strings.TrimSpace(input.Reason); reason != "" {
|
||||
args = append(args, reason)
|
||||
where = append(where, fmt.Sprintf("l.reason = $%d", len(args)))
|
||||
}
|
||||
if input.StartAt != nil {
|
||||
args = append(args, *input.StartAt)
|
||||
where = append(where, fmt.Sprintf("l.created_at >= $%d", len(args)))
|
||||
}
|
||||
if input.EndAt != nil {
|
||||
args = append(args, *input.EndAt)
|
||||
where = append(where, fmt.Sprintf("l.created_at < $%d", len(args)))
|
||||
}
|
||||
if keyword := strings.TrimSpace(input.Keyword); keyword != "" {
|
||||
needsSearchJoin = true
|
||||
args = append(args, "%"+keyword+"%")
|
||||
keywordArg := len(args)
|
||||
predicates := []string{
|
||||
fmt.Sprintf("COALESCE(u.phone, '') ILIKE $%d", keywordArg),
|
||||
fmt.Sprintf("COALESCE(u.email, '') ILIKE $%d", keywordArg),
|
||||
fmt.Sprintf("COALESCE(u.name, '') ILIKE $%d", keywordArg),
|
||||
fmt.Sprintf("COALESCE(t.name, '') ILIKE $%d", keywordArg),
|
||||
fmt.Sprintf("COALESCE(o.title, '') ILIKE $%d", keywordArg),
|
||||
fmt.Sprintf("COALESCE(o.external_order_code, '') ILIKE $%d", keywordArg),
|
||||
fmt.Sprintf("COALESCE(l.note, '') ILIKE $%d", keywordArg),
|
||||
}
|
||||
if id, err := strconv.ParseInt(keyword, 10, 64); err == nil && id > 0 {
|
||||
args = append(args, id)
|
||||
idArg := len(args)
|
||||
predicates = append(predicates, fmt.Sprintf("(l.tenant_id = $%d OR l.user_id = $%d OR l.order_id = $%d)", idArg, idArg, idArg))
|
||||
}
|
||||
where = append(where, "("+strings.Join(predicates, " OR ")+")")
|
||||
}
|
||||
whereSQL := strings.Join(where, " AND ")
|
||||
summarySQL := `
|
||||
SELECT COUNT(*),
|
||||
COALESCE(SUM(l.sales_cents), 0),
|
||||
COALESCE(SUM(l.cost_cents), 0),
|
||||
COALESCE(SUM(l.gross_profit_cents), 0),
|
||||
COALESCE(SUM(l.delta_cents), 0),
|
||||
COALESCE(SUM(CASE WHEN l.reason = 'order_debit' THEN -l.delta_cents ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN l.reason = 'order_refund' THEN l.delta_cents ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN l.reason = 'recharge' THEN l.delta_cents ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN l.reason = 'adjustment' THEN l.delta_cents ELSE 0 END), 0)
|
||||
FROM media_supply_wallet_ledgers l`
|
||||
if needsSearchJoin {
|
||||
summarySQL += `
|
||||
LEFT JOIN users u ON u.id = l.user_id
|
||||
LEFT JOIN tenants t ON t.id = l.tenant_id
|
||||
LEFT JOIN media_supply_orders o ON o.id = l.order_id`
|
||||
}
|
||||
summarySQL += `
|
||||
WHERE ` + whereSQL
|
||||
var summary MediaSupplyBillingSummary
|
||||
if err := s.pool.QueryRow(ctx, summarySQL, args...).Scan(
|
||||
&summary.TotalEntries,
|
||||
&summary.TotalSalesCents,
|
||||
&summary.TotalCostCents,
|
||||
&summary.GrossProfitCents,
|
||||
&summary.NetWalletDeltaCents,
|
||||
&summary.OrderDebitCents,
|
||||
&summary.OrderRefundCents,
|
||||
&summary.RechargeCents,
|
||||
&summary.ManualAdjustmentCents,
|
||||
); err != nil {
|
||||
return nil, response.ErrInternal(50091, "media_supply_billing_summary_failed", "媒体投稿账单汇总失败")
|
||||
}
|
||||
listArgs := append([]any{}, args...)
|
||||
listArgs = append(listArgs, size, (page-1)*size)
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT l.id, l.tenant_id, t.name, l.user_id, u.name, u.phone, l.order_id,
|
||||
o.title, o.status, o.external_order_code, l.delta_cents,
|
||||
l.balance_after_cents, l.reason, l.note, l.sales_cents, l.cost_cents,
|
||||
l.gross_profit_cents, l.created_by, l.created_at
|
||||
FROM media_supply_wallet_ledgers l
|
||||
LEFT JOIN users u ON u.id = l.user_id
|
||||
LEFT JOIN tenants t ON t.id = l.tenant_id
|
||||
LEFT JOIN media_supply_orders o ON o.id = l.order_id
|
||||
WHERE `+whereSQL+`
|
||||
ORDER BY l.created_at DESC, l.id DESC
|
||||
LIMIT $`+fmt.Sprint(len(listArgs)-1)+` OFFSET $`+fmt.Sprint(len(listArgs))+`
|
||||
`, listArgs...)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50092, "media_supply_billing_query_failed", "媒体投稿账单读取失败")
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]MediaSupplyBillingEntry, 0, size)
|
||||
for rows.Next() {
|
||||
var item MediaSupplyBillingEntry
|
||||
var tenantName sql.NullString
|
||||
var userName sql.NullString
|
||||
var userPhone sql.NullString
|
||||
var orderID sql.NullInt64
|
||||
var orderTitle sql.NullString
|
||||
var orderStatus sql.NullString
|
||||
var externalOrderCode sql.NullString
|
||||
var note sql.NullString
|
||||
var createdBy sql.NullInt64
|
||||
if err := rows.Scan(
|
||||
&item.ID,
|
||||
&item.TenantID,
|
||||
&tenantName,
|
||||
&item.UserID,
|
||||
&userName,
|
||||
&userPhone,
|
||||
&orderID,
|
||||
&orderTitle,
|
||||
&orderStatus,
|
||||
&externalOrderCode,
|
||||
&item.DeltaCents,
|
||||
&item.BalanceAfterCents,
|
||||
&item.Reason,
|
||||
¬e,
|
||||
&item.SalesCents,
|
||||
&item.CostCents,
|
||||
&item.GrossProfitCents,
|
||||
&createdBy,
|
||||
&item.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, response.ErrInternal(50093, "media_supply_billing_scan_failed", "媒体投稿账单解析失败")
|
||||
}
|
||||
if tenantName.Valid {
|
||||
item.TenantName = &tenantName.String
|
||||
}
|
||||
if userName.Valid {
|
||||
item.UserName = &userName.String
|
||||
}
|
||||
if userPhone.Valid {
|
||||
item.UserPhone = &userPhone.String
|
||||
}
|
||||
if orderID.Valid {
|
||||
item.OrderID = &orderID.Int64
|
||||
}
|
||||
if orderTitle.Valid {
|
||||
item.OrderTitle = &orderTitle.String
|
||||
}
|
||||
if orderStatus.Valid {
|
||||
item.OrderStatus = &orderStatus.String
|
||||
}
|
||||
if externalOrderCode.Valid {
|
||||
item.ExternalOrderCode = &externalOrderCode.String
|
||||
}
|
||||
if note.Valid {
|
||||
item.Note = ¬e.String
|
||||
}
|
||||
if createdBy.Valid {
|
||||
item.CreatedBy = &createdBy.Int64
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50093, "media_supply_billing_scan_failed", "媒体投稿账单解析失败")
|
||||
}
|
||||
return &ListMediaSupplyBillingsResult{
|
||||
Items: items,
|
||||
Total: summary.TotalEntries,
|
||||
Page: page,
|
||||
Size: size,
|
||||
Summary: summary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) QueueSync(ctx context.Context, actor *Actor, modelID int) (int64, error) {
|
||||
if s == nil || s.pool == nil {
|
||||
return 0, response.ErrServiceUnavailable(50360, "media_supply_store_unavailable", "媒体投稿服务暂不可用")
|
||||
@@ -455,9 +691,10 @@ func (s *MediaSupplyService) AdjustWallet(ctx context.Context, actor *Actor, inp
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO media_supply_wallet_ledgers (
|
||||
tenant_id, user_id, delta_cents, balance_after_cents, reason, note, created_by
|
||||
tenant_id, user_id, delta_cents, balance_after_cents, reason, note, created_by,
|
||||
sales_cents, cost_cents, gross_profit_cents
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NULL)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NULL, 0, 0, 0)
|
||||
`, input.TenantID, input.UserID, input.DeltaCents, balanceAfter, reason, nullableOpsMediaSupplyString(note)); err != nil {
|
||||
return nil, response.ErrInternal(50084, "media_supply_wallet_ledger_create_failed", "媒体投稿账单创建失败")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package transport
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -157,6 +158,58 @@ func listMediaSupplyWalletsHandler(svc *app.MediaSupplyService) gin.HandlerFunc
|
||||
}
|
||||
}
|
||||
|
||||
func listMediaSupplyBillingsHandler(svc *app.MediaSupplyService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tenantID, err := parseOptionalOpsInt64Query(c, "tenant_id")
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_tenant_id", "租户参数必须是数字"))
|
||||
return
|
||||
}
|
||||
userID, err := parseOptionalOpsInt64Query(c, "user_id")
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_user_id", "用户参数必须是数字"))
|
||||
return
|
||||
}
|
||||
orderID, err := parseOptionalOpsInt64Query(c, "order_id")
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_order_id", "订单参数必须是数字"))
|
||||
return
|
||||
}
|
||||
startAt, err := parseOptionalOpsTimeQuery(c, "start_at")
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40003, "invalid_start_at", "start_at 必须是 RFC3339"))
|
||||
return
|
||||
}
|
||||
endAt, err := parseOptionalOpsTimeQuery(c, "end_at")
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40004, "invalid_end_at", "end_at 必须是 RFC3339"))
|
||||
return
|
||||
}
|
||||
if startAt != nil && endAt != nil && !startAt.Before(*endAt) {
|
||||
response.Error(c, response.ErrBadRequest(40005, "invalid_billing_time_range", "结束时间必须晚于开始时间"))
|
||||
return
|
||||
}
|
||||
page, _ := parseOptionalOpsIntQuery(c, "page")
|
||||
size, _ := parseOptionalOpsIntQuery(c, "page_size")
|
||||
result, err := svc.ListBillings(c.Request.Context(), app.ListMediaSupplyBillingsInput{
|
||||
Keyword: c.Query("keyword"),
|
||||
TenantID: tenantID,
|
||||
UserID: userID,
|
||||
OrderID: orderID,
|
||||
Reason: c.Query("reason"),
|
||||
StartAt: startAt,
|
||||
EndAt: endAt,
|
||||
Page: page,
|
||||
Size: size,
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
}
|
||||
|
||||
func adjustMediaSupplyWalletHandler(svc *app.MediaSupplyService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var body adjustMediaSupplyWalletRequest
|
||||
@@ -199,3 +252,15 @@ func parseOptionalOpsInt64Query(c *gin.Context, key string) (int64, error) {
|
||||
}
|
||||
return strconv.ParseInt(raw, 10, 64)
|
||||
}
|
||||
|
||||
func parseOptionalOpsTimeQuery(c *gin.Context, key string) (*time.Time, error) {
|
||||
raw := c.Query(key)
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ func RegisterRoutes(d Deps) {
|
||||
authed.PUT("/media-supply/resources/:id/visibility", setMediaSupplyResourceVisibilityHandler(d.MediaSupply))
|
||||
authed.POST("/media-supply/sync-jobs", queueMediaSupplySyncHandler(d.MediaSupply))
|
||||
authed.GET("/media-supply/wallets", listMediaSupplyWalletsHandler(d.MediaSupply))
|
||||
authed.GET("/media-supply/billings", listMediaSupplyBillingsHandler(d.MediaSupply))
|
||||
authed.POST("/media-supply/wallets/adjustments", adjustMediaSupplyWalletHandler(d.MediaSupply))
|
||||
|
||||
authed.GET("/scheduler/jobs", listSchedulerJobsHandler(d.Scheduler))
|
||||
|
||||
@@ -763,9 +763,10 @@ func (s *MediaSupplyService) AdjustWallet(ctx context.Context, req AdjustMediaSu
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO media_supply_wallet_ledgers (
|
||||
tenant_id, user_id, delta_cents, balance_after_cents, reason, note, created_by
|
||||
tenant_id, user_id, delta_cents, balance_after_cents, reason, note, created_by,
|
||||
sales_cents, cost_cents, gross_profit_cents
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 0, 0, 0)
|
||||
`, actor.TenantID, req.UserID, req.DeltaCents, balanceAfter, reason, nullableTrimmedString(req.Note), actor.UserID); err != nil {
|
||||
return nil, response.ErrInternal(50084, "media_supply_wallet_ledger_create_failed", "媒体投稿账单创建失败")
|
||||
}
|
||||
@@ -848,9 +849,15 @@ func (s *MediaSupplyService) debitMediaSupplyWallet(ctx context.Context, tx pgx.
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO media_supply_wallet_ledgers (
|
||||
tenant_id, user_id, order_id, delta_cents, balance_after_cents, reason, note, created_by
|
||||
tenant_id, user_id, order_id, delta_cents, balance_after_cents, reason, note, created_by,
|
||||
sales_cents, cost_cents, gross_profit_cents
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8,
|
||||
(SELECT sell_total_cents FROM media_supply_orders WHERE id = $3),
|
||||
(SELECT cost_total_cents FROM media_supply_orders WHERE id = $3),
|
||||
(SELECT sell_total_cents - cost_total_cents FROM media_supply_orders WHERE id = $3)
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`, tenantID, userID, orderID, -amountCents, balanceAfter, "order_debit", nullableTrimmedString(orderTitle), createdBy); err != nil {
|
||||
return 0, response.ErrInternal(50084, "media_supply_wallet_ledger_create_failed", "媒体投稿账单创建失败")
|
||||
}
|
||||
@@ -900,9 +907,15 @@ func (s *MediaSupplyService) refundMediaSupplyWalletForOrder(ctx context.Context
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO media_supply_wallet_ledgers (
|
||||
tenant_id, user_id, order_id, delta_cents, balance_after_cents, reason, note, created_by
|
||||
tenant_id, user_id, order_id, delta_cents, balance_after_cents, reason, note, created_by,
|
||||
sales_cents, cost_cents, gross_profit_cents
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8,
|
||||
(SELECT -sell_total_cents FROM media_supply_orders WHERE id = $3),
|
||||
(SELECT -cost_total_cents FROM media_supply_orders WHERE id = $3),
|
||||
(SELECT -(sell_total_cents - cost_total_cents) FROM media_supply_orders WHERE id = $3)
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`, tenantID, userID, orderID, debitCents, balanceAfter, "order_refund", nullableTrimmedString(ledgerNote), userID); err != nil {
|
||||
return response.ErrInternal(50084, "media_supply_wallet_ledger_create_failed", "媒体投稿账单创建失败")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE media_supply_wallet_ledgers
|
||||
DROP CONSTRAINT IF EXISTS chk_media_supply_wallet_ledger_fact_non_negative,
|
||||
DROP COLUMN IF EXISTS gross_profit_cents,
|
||||
DROP COLUMN IF EXISTS cost_cents,
|
||||
DROP COLUMN IF EXISTS sales_cents;
|
||||
@@ -0,0 +1,34 @@
|
||||
ALTER TABLE media_supply_wallet_ledgers
|
||||
ADD COLUMN IF NOT EXISTS sales_cents BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS cost_cents BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS gross_profit_cents BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE media_supply_wallet_ledgers l
|
||||
SET sales_cents = CASE
|
||||
WHEN l.reason = 'order_debit' THEN o.sell_total_cents
|
||||
WHEN l.reason = 'order_refund' THEN -o.sell_total_cents
|
||||
ELSE 0
|
||||
END,
|
||||
cost_cents = CASE
|
||||
WHEN l.reason = 'order_debit' THEN o.cost_total_cents
|
||||
WHEN l.reason = 'order_refund' THEN -o.cost_total_cents
|
||||
ELSE 0
|
||||
END,
|
||||
gross_profit_cents = CASE
|
||||
WHEN l.reason = 'order_debit' THEN o.sell_total_cents - o.cost_total_cents
|
||||
WHEN l.reason = 'order_refund' THEN -(o.sell_total_cents - o.cost_total_cents)
|
||||
ELSE 0
|
||||
END
|
||||
FROM media_supply_orders o
|
||||
WHERE l.order_id = o.id
|
||||
AND l.reason IN ('order_debit', 'order_refund');
|
||||
|
||||
ALTER TABLE media_supply_wallet_ledgers
|
||||
ADD CONSTRAINT chk_media_supply_wallet_ledger_fact_non_negative
|
||||
CHECK (
|
||||
reason = 'order_refund'
|
||||
OR (sales_cents >= 0 AND cost_cents >= 0 AND gross_profit_cents >= 0)
|
||||
) NOT VALID;
|
||||
|
||||
ALTER TABLE media_supply_wallet_ledgers
|
||||
VALIDATE CONSTRAINT chk_media_supply_wallet_ledger_fact_non_negative;
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX CONCURRENTLY IF EXISTS idx_media_supply_wallet_ledgers_created_id;
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_media_supply_wallet_ledgers_created_id
|
||||
ON media_supply_wallet_ledgers (created_at DESC, id DESC)
|
||||
INCLUDE (
|
||||
tenant_id,
|
||||
user_id,
|
||||
order_id,
|
||||
reason,
|
||||
delta_cents,
|
||||
sales_cents,
|
||||
cost_cents,
|
||||
gross_profit_cents
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX CONCURRENTLY IF EXISTS idx_media_supply_wallet_ledgers_tenant_created;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_media_supply_wallet_ledgers_tenant_created
|
||||
ON media_supply_wallet_ledgers (tenant_id, created_at DESC, id DESC)
|
||||
INCLUDE (
|
||||
user_id,
|
||||
order_id,
|
||||
reason,
|
||||
delta_cents,
|
||||
sales_cents,
|
||||
cost_cents,
|
||||
gross_profit_cents
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX CONCURRENTLY IF EXISTS idx_media_supply_wallet_ledgers_tenant_user_created_cover;
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_media_supply_wallet_ledgers_tenant_user_created_cover
|
||||
ON media_supply_wallet_ledgers (tenant_id, user_id, created_at DESC, id DESC)
|
||||
INCLUDE (
|
||||
order_id,
|
||||
reason,
|
||||
delta_cents,
|
||||
sales_cents,
|
||||
cost_cents,
|
||||
gross_profit_cents
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX CONCURRENTLY IF EXISTS idx_media_supply_wallet_ledgers_reason_created;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_media_supply_wallet_ledgers_reason_created
|
||||
ON media_supply_wallet_ledgers (reason, created_at DESC, id DESC)
|
||||
INCLUDE (
|
||||
tenant_id,
|
||||
user_id,
|
||||
order_id,
|
||||
delta_cents,
|
||||
sales_cents,
|
||||
cost_cents,
|
||||
gross_profit_cents
|
||||
);
|
||||
Reference in New Issue
Block a user