mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(payments): 增加兑换码与支付适配框架 (#299)
* feat(payments): 增加兑换码与支付适配框架 * fix(ci): 对齐 Rust 1.95 lint 与格式要求 * fix(payments): harden redeem code wallet credits --------- Co-authored-by: fawney19 <elky0401@gmail.com>
This commit is contained in:
@@ -4766,3 +4766,120 @@ COMMENT ON COLUMN public.usage.client_response_body IS
|
||||
'DEPRECATED: HTTP body owner moved to public.usage_body_blobs plus public.usage_http_audits.client_response_body_ref. Legacy compatibility only; do not write new values.';
|
||||
COMMENT ON COLUMN public.usage.client_response_body_compressed IS
|
||||
'DEPRECATED: HTTP body owner moved to public.usage_body_blobs plus public.usage_http_audits.client_response_body_ref. Legacy compatibility only; do not write new values.';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.redeem_code_batches (
|
||||
id character varying(36) NOT NULL,
|
||||
name character varying(120) NOT NULL,
|
||||
amount_usd numeric(20,8) NOT NULL,
|
||||
currency character varying(3) DEFAULT 'USD'::character varying NOT NULL,
|
||||
balance_bucket character varying(20) DEFAULT 'gift'::character varying NOT NULL,
|
||||
total_count integer NOT NULL,
|
||||
status character varying(20) DEFAULT 'active'::character varying NOT NULL,
|
||||
description text,
|
||||
created_by character varying(36),
|
||||
expires_at timestamp with time zone,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
CONSTRAINT ck_redeem_code_batches_amount_positive CHECK ((amount_usd > (0)::numeric)),
|
||||
CONSTRAINT ck_redeem_code_batches_total_count_positive CHECK ((total_count > 0))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.redeem_codes (
|
||||
id character varying(36) NOT NULL,
|
||||
batch_id character varying(36) NOT NULL,
|
||||
code_hash character varying(64) NOT NULL,
|
||||
code_prefix character varying(8) NOT NULL,
|
||||
code_suffix character varying(8) NOT NULL,
|
||||
status character varying(20) DEFAULT 'active'::character varying NOT NULL,
|
||||
redeemed_by_user_id character varying(36),
|
||||
redeemed_wallet_id character varying(36),
|
||||
redeemed_payment_order_id character varying(36),
|
||||
redeemed_at timestamp with time zone,
|
||||
disabled_by character varying(36),
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_code_batches
|
||||
ADD CONSTRAINT redeem_code_batches_pkey PRIMARY KEY (id);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_pkey PRIMARY KEY (id);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT uq_redeem_codes_code_hash UNIQUE (code_hash);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_code_batches_status
|
||||
ON public.redeem_code_batches USING btree (status, created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_batch_created
|
||||
ON public.redeem_codes USING btree (batch_id, created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_status
|
||||
ON public.redeem_codes USING btree (status, updated_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_user
|
||||
ON public.redeem_codes USING btree (redeemed_by_user_id, redeemed_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_order
|
||||
ON public.redeem_codes USING btree (redeemed_payment_order_id);
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_code_batches
|
||||
ADD CONSTRAINT redeem_code_batches_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_batch_id_fkey FOREIGN KEY (batch_id) REFERENCES public.redeem_code_batches(id) ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_redeemed_by_user_id_fkey FOREIGN KEY (redeemed_by_user_id) REFERENCES public.users(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_redeemed_wallet_id_fkey FOREIGN KEY (redeemed_wallet_id) REFERENCES public.wallets(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_redeemed_payment_order_id_fkey FOREIGN KEY (redeemed_payment_order_id) REFERENCES public.payment_orders(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_disabled_by_fkey FOREIGN KEY (disabled_by) REFERENCES public.users(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
CREATE TABLE IF NOT EXISTS public.redeem_code_batches (
|
||||
id character varying(36) NOT NULL,
|
||||
name character varying(120) NOT NULL,
|
||||
amount_usd numeric(20,8) NOT NULL,
|
||||
currency character varying(3) DEFAULT 'USD'::character varying NOT NULL,
|
||||
balance_bucket character varying(20) DEFAULT 'gift'::character varying NOT NULL,
|
||||
total_count integer NOT NULL,
|
||||
status character varying(20) DEFAULT 'active'::character varying NOT NULL,
|
||||
description text,
|
||||
created_by character varying(36),
|
||||
expires_at timestamp with time zone,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
CONSTRAINT ck_redeem_code_batches_amount_positive CHECK ((amount_usd > (0)::numeric)),
|
||||
CONSTRAINT ck_redeem_code_batches_total_count_positive CHECK ((total_count > 0))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.redeem_codes (
|
||||
id character varying(36) NOT NULL,
|
||||
batch_id character varying(36) NOT NULL,
|
||||
code_hash character varying(64) NOT NULL,
|
||||
code_prefix character varying(8) NOT NULL,
|
||||
code_suffix character varying(8) NOT NULL,
|
||||
status character varying(20) DEFAULT 'active'::character varying NOT NULL,
|
||||
redeemed_by_user_id character varying(36),
|
||||
redeemed_wallet_id character varying(36),
|
||||
redeemed_payment_order_id character varying(36),
|
||||
redeemed_at timestamp with time zone,
|
||||
disabled_by character varying(36),
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_code_batches
|
||||
ADD CONSTRAINT redeem_code_batches_pkey PRIMARY KEY (id);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_pkey PRIMARY KEY (id);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT uq_redeem_codes_code_hash UNIQUE (code_hash);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_code_batches_status
|
||||
ON public.redeem_code_batches USING btree (status, created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_batch_created
|
||||
ON public.redeem_codes USING btree (batch_id, created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_status
|
||||
ON public.redeem_codes USING btree (status, updated_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_user
|
||||
ON public.redeem_codes USING btree (redeemed_by_user_id, redeemed_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_order
|
||||
ON public.redeem_codes USING btree (redeemed_payment_order_id);
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_code_batches
|
||||
ADD CONSTRAINT redeem_code_batches_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_batch_id_fkey FOREIGN KEY (batch_id) REFERENCES public.redeem_code_batches(id) ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_redeemed_by_user_id_fkey FOREIGN KEY (redeemed_by_user_id) REFERENCES public.users(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_redeemed_wallet_id_fkey FOREIGN KEY (redeemed_wallet_id) REFERENCES public.wallets(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_redeemed_payment_order_id_fkey FOREIGN KEY (redeemed_payment_order_id) REFERENCES public.payment_orders(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.redeem_codes
|
||||
ADD CONSTRAINT redeem_codes_disabled_by_fkey FOREIGN KEY (disabled_by) REFERENCES public.users(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $mig$;
|
||||
@@ -8,7 +8,7 @@ use tracing::{error, info, warn};
|
||||
|
||||
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||
static BASELINE_V2_SQL: &str = include_str!("../bootstrap/20260413020000_baseline_v2.sql");
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260413030000;
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260415000000;
|
||||
const MIGRATIONS_TABLE_EXISTS_SQL: &str =
|
||||
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL";
|
||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||
@@ -597,6 +597,7 @@ mod tests {
|
||||
20260410000000,
|
||||
20260413020000,
|
||||
20260413030000,
|
||||
20260415000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -687,7 +688,12 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
pending_versions,
|
||||
vec![20260410000000, 20260413020000, 20260413030000]
|
||||
vec![
|
||||
20260410000000,
|
||||
20260413020000,
|
||||
20260413030000,
|
||||
20260415000000
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ impl RequestCandidateReadRepository for InMemoryRequestCandidateRepository {
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
rows.sort_by_key(|right| std::cmp::Reverse(right.created_at_unix_ms));
|
||||
rows.sort_by_key(|entry| std::cmp::Reverse(entry.created_at_unix_ms));
|
||||
rows.truncate(limit);
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -90,7 +90,7 @@ impl RequestCandidateReadRepository for InMemoryRequestCandidateRepository {
|
||||
.filter(|row| row.provider_id.as_deref() == Some(provider_id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
rows.sort_by_key(|right| std::cmp::Reverse(right.created_at_unix_ms));
|
||||
rows.sort_by_key(|entry| std::cmp::Reverse(entry.created_at_unix_ms));
|
||||
rows.truncate(limit);
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -125,7 +125,7 @@ impl RequestCandidateReadRepository for InMemoryRequestCandidateRepository {
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
rows.sort_by_key(|right| std::cmp::Reverse(right.created_at_unix_ms));
|
||||
rows.sort_by_key(|entry| std::cmp::Reverse(entry.created_at_unix_ms));
|
||||
rows.truncate(limit);
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ impl VideoTaskReadRepository for InMemoryVideoTaskRepository {
|
||||
.filter(|task| task.status.is_active())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
tasks.sort_by_key(|right| std::cmp::Reverse(right.updated_at_unix_secs));
|
||||
tasks.sort_by_key(|entry| std::cmp::Reverse(entry.updated_at_unix_secs));
|
||||
tasks.truncate(limit);
|
||||
Ok(tasks)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,16 +6,22 @@ pub use memory::InMemoryWalletRepository;
|
||||
pub use sql::SqlxWalletRepository;
|
||||
pub use types::{
|
||||
AdjustWalletBalanceInput, AdminPaymentCallbackRecord, AdminPaymentOrderListQuery,
|
||||
AdminWalletLedgerQuery, AdminWalletListQuery, AdminWalletPaymentOrderRecord,
|
||||
AdminWalletRefundRecord, AdminWalletRefundRequestListQuery, AdminWalletTransactionRecord,
|
||||
CompleteAdminWalletRefundInput, CreateManualWalletRechargeInput,
|
||||
AdminRedeemCodeBatchListQuery, AdminRedeemCodeListQuery, AdminWalletLedgerQuery,
|
||||
AdminWalletListQuery, AdminWalletPaymentOrderRecord, AdminWalletRefundRecord,
|
||||
AdminWalletRefundRequestListQuery, AdminWalletTransactionRecord,
|
||||
CompleteAdminWalletRefundInput, CreateAdminRedeemCodeBatchInput,
|
||||
CreateAdminRedeemCodeBatchResult, CreateManualWalletRechargeInput,
|
||||
CreateWalletRechargeOrderInput, CreateWalletRechargeOrderOutcome,
|
||||
CreateWalletRefundRequestInput, CreateWalletRefundRequestOutcome, CreditAdminPaymentOrderInput,
|
||||
FailAdminWalletRefundInput, ProcessAdminWalletRefundInput, ProcessPaymentCallbackInput,
|
||||
ProcessPaymentCallbackOutcome, StoredAdminPaymentCallback, StoredAdminPaymentCallbackPage,
|
||||
StoredAdminPaymentOrder, StoredAdminPaymentOrderPage, StoredAdminWalletLedgerItem,
|
||||
StoredAdminWalletLedgerPage, StoredAdminWalletListItem, StoredAdminWalletListPage,
|
||||
StoredAdminWalletRefund, StoredAdminWalletRefundPage, StoredAdminWalletRefundRequestItem,
|
||||
CreateWalletRefundRequestInput, CreateWalletRefundRequestOutcome,
|
||||
CreatedAdminRedeemCodePlaintext, CreditAdminPaymentOrderInput, DeleteAdminRedeemCodeBatchInput,
|
||||
DisableAdminRedeemCodeBatchInput, DisableAdminRedeemCodeInput, FailAdminWalletRefundInput,
|
||||
ProcessAdminWalletRefundInput, ProcessPaymentCallbackInput, ProcessPaymentCallbackOutcome,
|
||||
RedeemWalletCodeInput, RedeemWalletCodeOutcome, StoredAdminPaymentCallback,
|
||||
StoredAdminPaymentCallbackPage, StoredAdminPaymentOrder, StoredAdminPaymentOrderPage,
|
||||
StoredAdminRedeemCode, StoredAdminRedeemCodeBatch, StoredAdminRedeemCodeBatchPage,
|
||||
StoredAdminRedeemCodePage, StoredAdminWalletLedgerItem, StoredAdminWalletLedgerPage,
|
||||
StoredAdminWalletListItem, StoredAdminWalletListPage, StoredAdminWalletRefund,
|
||||
StoredAdminWalletRefundPage, StoredAdminWalletRefundRequestItem,
|
||||
StoredAdminWalletRefundRequestPage, StoredAdminWalletTransaction,
|
||||
StoredAdminWalletTransactionPage, StoredWalletDailyUsageLedger,
|
||||
StoredWalletDailyUsageLedgerPage, StoredWalletSnapshot, WalletLookupKey, WalletMutationOutcome,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -443,6 +443,160 @@ pub struct StoredAdminPaymentCallbackPage {
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AdminRedeemCodeBatchListQuery {
|
||||
pub status: Option<String>,
|
||||
pub limit: usize,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredAdminRedeemCodeBatch {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub amount_usd: f64,
|
||||
pub currency: String,
|
||||
pub balance_bucket: String,
|
||||
pub total_count: u64,
|
||||
pub redeemed_count: u64,
|
||||
pub active_count: u64,
|
||||
pub status: String,
|
||||
pub description: Option<String>,
|
||||
pub created_by: Option<String>,
|
||||
pub expires_at_unix_secs: Option<u64>,
|
||||
pub created_at_unix_ms: u64,
|
||||
pub updated_at_unix_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredAdminRedeemCodeBatchPage {
|
||||
pub items: Vec<StoredAdminRedeemCodeBatch>,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AdminRedeemCodeListQuery {
|
||||
pub batch_id: String,
|
||||
pub status: Option<String>,
|
||||
pub limit: usize,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredAdminRedeemCode {
|
||||
pub id: String,
|
||||
pub batch_id: String,
|
||||
pub batch_name: Option<String>,
|
||||
pub code_prefix: String,
|
||||
pub code_suffix: String,
|
||||
pub masked_code: String,
|
||||
pub status: String,
|
||||
pub redeemed_by_user_id: Option<String>,
|
||||
pub redeemed_by_user_name: Option<String>,
|
||||
pub redeemed_wallet_id: Option<String>,
|
||||
pub redeemed_payment_order_id: Option<String>,
|
||||
pub redeemed_order_no: Option<String>,
|
||||
pub redeemed_at_unix_secs: Option<u64>,
|
||||
pub disabled_by: Option<String>,
|
||||
pub expires_at_unix_secs: Option<u64>,
|
||||
pub created_at_unix_ms: u64,
|
||||
pub updated_at_unix_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredAdminRedeemCodePage {
|
||||
pub items: Vec<StoredAdminRedeemCode>,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CreatedAdminRedeemCodePlaintext {
|
||||
pub code_id: String,
|
||||
pub code: String,
|
||||
pub masked_code: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CreateAdminRedeemCodeBatchInput {
|
||||
pub name: String,
|
||||
pub amount_usd: f64,
|
||||
pub currency: String,
|
||||
pub balance_bucket: String,
|
||||
pub total_count: usize,
|
||||
pub expires_at_unix_secs: Option<u64>,
|
||||
pub description: Option<String>,
|
||||
pub created_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CreateAdminRedeemCodeBatchResult {
|
||||
pub batch: StoredAdminRedeemCodeBatch,
|
||||
pub codes: Vec<CreatedAdminRedeemCodePlaintext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DisableAdminRedeemCodeBatchInput {
|
||||
pub batch_id: String,
|
||||
pub operator_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DeleteAdminRedeemCodeBatchInput {
|
||||
pub batch_id: String,
|
||||
pub operator_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DisableAdminRedeemCodeInput {
|
||||
pub code_id: String,
|
||||
pub operator_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RedeemWalletCodeInput {
|
||||
pub code: String,
|
||||
pub user_id: String,
|
||||
pub order_no: String,
|
||||
}
|
||||
|
||||
pub(crate) fn redeem_code_credits_recharge_balance(balance_bucket: &str) -> bool {
|
||||
balance_bucket.trim().eq_ignore_ascii_case("recharge")
|
||||
}
|
||||
|
||||
pub(crate) fn redeem_code_payment_method(balance_bucket: &str) -> &'static str {
|
||||
if redeem_code_credits_recharge_balance(balance_bucket) {
|
||||
"card_code"
|
||||
} else {
|
||||
"gift_code"
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn redeem_code_refundable_amount(balance_bucket: &str, amount_usd: f64) -> f64 {
|
||||
if redeem_code_credits_recharge_balance(balance_bucket) {
|
||||
amount_usd
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum RedeemWalletCodeOutcome {
|
||||
Redeemed {
|
||||
wallet: StoredWalletSnapshot,
|
||||
order: StoredAdminPaymentOrder,
|
||||
amount_usd: f64,
|
||||
batch_name: String,
|
||||
},
|
||||
InvalidCode,
|
||||
CodeNotFound,
|
||||
CodeDisabled,
|
||||
BatchDisabled,
|
||||
CodeExpired,
|
||||
CodeRedeemed,
|
||||
WalletInactive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CreateWalletRechargeOrderInput {
|
||||
pub preferred_wallet_id: Option<String>,
|
||||
@@ -686,6 +840,21 @@ pub trait WalletReadRepository: Send + Sync {
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<StoredAdminPaymentCallbackPage, crate::DataLayerError>;
|
||||
|
||||
async fn list_admin_redeem_code_batches(
|
||||
&self,
|
||||
query: &AdminRedeemCodeBatchListQuery,
|
||||
) -> Result<StoredAdminRedeemCodeBatchPage, crate::DataLayerError>;
|
||||
|
||||
async fn find_admin_redeem_code_batch(
|
||||
&self,
|
||||
batch_id: &str,
|
||||
) -> Result<Option<StoredAdminRedeemCodeBatch>, crate::DataLayerError>;
|
||||
|
||||
async fn list_admin_redeem_codes(
|
||||
&self,
|
||||
query: &AdminRedeemCodeListQuery,
|
||||
) -> Result<StoredAdminRedeemCodePage, crate::DataLayerError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -758,6 +927,31 @@ pub trait WalletWriteRepository: Send + Sync {
|
||||
&self,
|
||||
input: CreditAdminPaymentOrderInput,
|
||||
) -> Result<WalletMutationOutcome<(StoredAdminPaymentOrder, bool)>, crate::DataLayerError>;
|
||||
|
||||
async fn create_admin_redeem_code_batch(
|
||||
&self,
|
||||
input: CreateAdminRedeemCodeBatchInput,
|
||||
) -> Result<CreateAdminRedeemCodeBatchResult, crate::DataLayerError>;
|
||||
|
||||
async fn disable_admin_redeem_code_batch(
|
||||
&self,
|
||||
input: DisableAdminRedeemCodeBatchInput,
|
||||
) -> Result<WalletMutationOutcome<StoredAdminRedeemCodeBatch>, crate::DataLayerError>;
|
||||
|
||||
async fn delete_admin_redeem_code_batch(
|
||||
&self,
|
||||
input: DeleteAdminRedeemCodeBatchInput,
|
||||
) -> Result<WalletMutationOutcome<StoredAdminRedeemCodeBatch>, crate::DataLayerError>;
|
||||
|
||||
async fn disable_admin_redeem_code(
|
||||
&self,
|
||||
input: DisableAdminRedeemCodeInput,
|
||||
) -> Result<WalletMutationOutcome<StoredAdminRedeemCode>, crate::DataLayerError>;
|
||||
|
||||
async fn redeem_wallet_code(
|
||||
&self,
|
||||
input: RedeemWalletCodeInput,
|
||||
) -> Result<RedeemWalletCodeOutcome, crate::DataLayerError>;
|
||||
}
|
||||
|
||||
pub trait WalletRepository: WalletReadRepository + WalletWriteRepository + Send + Sync {}
|
||||
@@ -766,7 +960,10 @@ impl<T> WalletRepository for T where T: WalletReadRepository + WalletWriteReposi
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::StoredWalletSnapshot;
|
||||
use super::{
|
||||
redeem_code_credits_recharge_balance, redeem_code_payment_method,
|
||||
redeem_code_refundable_amount, StoredWalletSnapshot,
|
||||
};
|
||||
use crate::repository::settlement::UsageSettlementInput;
|
||||
|
||||
#[test]
|
||||
@@ -804,4 +1001,20 @@ mod tests {
|
||||
};
|
||||
assert!(input.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redeem_code_bucket_defaults_to_non_refundable_gift_semantics() {
|
||||
assert!(!redeem_code_credits_recharge_balance("gift"));
|
||||
assert_eq!(redeem_code_payment_method("gift"), "gift_code");
|
||||
assert_eq!(redeem_code_refundable_amount("gift", 8.5), 0.0);
|
||||
assert_eq!(redeem_code_payment_method("mystery"), "gift_code");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recharge_bucket_preserves_refundable_recharge_semantics() {
|
||||
assert!(redeem_code_credits_recharge_balance("recharge"));
|
||||
assert!(redeem_code_credits_recharge_balance(" Recharge "));
|
||||
assert_eq!(redeem_code_payment_method("recharge"), "card_code");
|
||||
assert_eq!(redeem_code_refundable_amount("recharge", 8.5), 8.5);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user