1use super::{
4 map_db_error, parse_datetime_opt_row, parse_datetime_row, parse_decimal_row, parse_enum_row,
5 parse_uuid_row, with_immediate_transaction,
6};
7use chrono::Utc;
8use r2d2::Pool;
9use r2d2_sqlite::SqliteConnectionManager;
10use rust_decimal::Decimal;
11use stateset_core::{
12 CommerceError, CreateGiftCard, GiftCard, GiftCardFilter, GiftCardId, GiftCardRepository,
13 GiftCardStatus, GiftCardTransaction, GiftCardTransactionId, Result, UpdateGiftCard,
14};
15
16#[derive(Debug)]
17pub struct SqliteGiftCardRepository {
18 pool: Pool<SqliteConnectionManager>,
19}
20
21impl SqliteGiftCardRepository {
22 #[must_use]
23 pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
24 Self { pool }
25 }
26
27 fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
28 self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))
29 }
30
31 fn row_to_gift_card(row: &rusqlite::Row<'_>) -> rusqlite::Result<GiftCard> {
32 Ok(GiftCard {
33 id: parse_uuid_row(&row.get::<_, String>("id")?, "gift_card", "id")?.into(),
34 code: row.get("code")?,
35 initial_balance: parse_decimal_row(
36 &row.get::<_, String>("initial_balance")?,
37 "gift_card",
38 "initial_balance",
39 )?,
40 current_balance: parse_decimal_row(
41 &row.get::<_, String>("current_balance")?,
42 "gift_card",
43 "current_balance",
44 )?,
45 currency: parse_enum_row(&row.get::<_, String>("currency")?, "gift_card", "currency")?,
46 status: parse_enum_row(&row.get::<_, String>("status")?, "gift_card", "status")?,
47 recipient_email: row.get("customer_id")?,
48 sender_name: row.get("issued_by")?,
49 message: row.get("notes")?,
50 expires_at: parse_datetime_opt_row(row.get("expires_at")?, "gift_card", "expires_at")?,
51 created_at: parse_datetime_row(
52 &row.get::<_, String>("created_at")?,
53 "gift_card",
54 "created_at",
55 )?,
56 updated_at: parse_datetime_row(
57 &row.get::<_, String>("updated_at")?,
58 "gift_card",
59 "updated_at",
60 )?,
61 })
62 }
63
64 fn row_to_transaction(row: &rusqlite::Row<'_>) -> rusqlite::Result<GiftCardTransaction> {
65 Ok(GiftCardTransaction {
66 id: parse_uuid_row(&row.get::<_, String>("id")?, "gift_card_txn", "id")?.into(),
67 gift_card_id: parse_uuid_row(
68 &row.get::<_, String>("gift_card_id")?,
69 "gift_card_txn",
70 "gift_card_id",
71 )?
72 .into(),
73 amount: parse_decimal_row(&row.get::<_, String>("amount")?, "gift_card_txn", "amount")?,
74 balance_after: parse_decimal_row(
75 &row.get::<_, String>("balance_after")?,
76 "gift_card_txn",
77 "balance_after",
78 )?,
79 transaction_type: parse_enum_row(
80 &row.get::<_, String>("type")?,
81 "gift_card_txn",
82 "type",
83 )?,
84 reference_id: row.get("reference_id")?,
85 created_at: parse_datetime_row(
86 &row.get::<_, String>("created_at")?,
87 "gift_card_txn",
88 "created_at",
89 )?,
90 })
91 }
92
93 fn generate_code() -> String {
95 let id = uuid::Uuid::new_v4();
96 let hex = id.simple().to_string();
97 let short = &hex[..16];
98 format!("{}-{}-{}-{}", &short[0..4], &short[4..8], &short[8..12], &short[12..16])
99 .to_uppercase()
100 }
101}
102
103impl GiftCardRepository for SqliteGiftCardRepository {
104 fn create(&self, input: CreateGiftCard) -> Result<GiftCard> {
105 if input.initial_balance < Decimal::ZERO {
109 return Err(CommerceError::ValidationError(
110 "Gift card initial balance cannot be negative".to_string(),
111 ));
112 }
113
114 let id = GiftCardId::new();
115 let now = Utc::now();
116 let id_str = id.to_string();
117 let now_str = now.to_rfc3339();
118 let code = input.code.unwrap_or_else(Self::generate_code);
119
120 with_immediate_transaction(&self.pool, |tx| {
121 tx.execute(
122 "INSERT INTO gift_cards (id, code, initial_balance, current_balance, currency, status, customer_id, issued_by, notes, expires_at, created_at, updated_at)
123 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
124 rusqlite::params![
125 &id_str,
126 &code,
127 input.initial_balance.to_string(),
128 input.initial_balance.to_string(),
129 input.currency.to_string(),
130 "active",
131 &input.recipient_email,
132 &input.sender_name,
133 &input.message,
134 input.expires_at.map(|dt| dt.to_rfc3339()),
135 &now_str,
136 &now_str,
137 ],
138 )?;
139
140 tx.query_row("SELECT * FROM gift_cards WHERE id = ?", [&id_str], Self::row_to_gift_card)
141 })
142 }
143
144 fn get(&self, id: GiftCardId) -> Result<Option<GiftCard>> {
145 let conn = self.conn()?;
146 match conn.query_row(
147 "SELECT * FROM gift_cards WHERE id = ?",
148 [id.to_string()],
149 Self::row_to_gift_card,
150 ) {
151 Ok(gc) => Ok(Some(gc)),
152 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
153 Err(e) => Err(map_db_error(e)),
154 }
155 }
156
157 fn get_by_code(&self, code: &str) -> Result<Option<GiftCard>> {
158 let conn = self.conn()?;
159 match conn.query_row(
160 "SELECT * FROM gift_cards WHERE code = ?",
161 [code],
162 Self::row_to_gift_card,
163 ) {
164 Ok(gc) => Ok(Some(gc)),
165 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
166 Err(e) => Err(map_db_error(e)),
167 }
168 }
169
170 fn update(&self, id: GiftCardId, input: UpdateGiftCard) -> Result<GiftCard> {
171 let id_str = id.to_string();
172 let now_str = Utc::now().to_rfc3339();
173
174 with_immediate_transaction(&self.pool, |tx| {
175 let mut sets = vec!["updated_at = ?".to_string()];
176 let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![Box::new(now_str.clone())];
177
178 if let Some(status) = input.status {
179 sets.push("status = ?".into());
180 params.push(Box::new(status.to_string()));
181 }
182 if let Some(ref recipient_email) = input.recipient_email {
183 sets.push("customer_id = ?".into());
184 params.push(Box::new(recipient_email.clone()));
185 }
186 if let Some(ref expires_at) = input.expires_at {
187 sets.push("expires_at = ?".into());
188 params.push(Box::new(expires_at.map(|dt| dt.to_rfc3339())));
189 }
190
191 let sql = format!("UPDATE gift_cards SET {} WHERE id = ?", sets.join(", "));
192 params.push(Box::new(id_str.clone()));
193
194 let param_refs: Vec<&dyn rusqlite::types::ToSql> =
195 params.iter().map(|p| p.as_ref()).collect();
196 tx.execute(&sql, param_refs.as_slice())?;
197
198 tx.query_row("SELECT * FROM gift_cards WHERE id = ?", [&id_str], Self::row_to_gift_card)
199 })
200 }
201
202 fn list(&self, filter: GiftCardFilter) -> Result<Vec<GiftCard>> {
203 let conn = self.conn()?;
204 let mut sql = "SELECT * FROM gift_cards WHERE 1=1".to_string();
205 let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
206
207 if let Some(status) = filter.status {
208 sql.push_str(" AND status = ?");
209 params.push(Box::new(status.to_string()));
210 }
211 if let Some(ref code) = filter.code {
212 sql.push_str(" AND code = ?");
213 params.push(Box::new(code.clone()));
214 }
215
216 sql.push_str(" ORDER BY created_at DESC");
217
218 match (filter.limit, filter.offset) {
221 (Some(limit), Some(offset)) => sql.push_str(&format!(" LIMIT {limit} OFFSET {offset}")),
222 (Some(limit), None) => sql.push_str(&format!(" LIMIT {limit}")),
223 (None, Some(offset)) => sql.push_str(&format!(" LIMIT -1 OFFSET {offset}")),
224 (None, None) => {}
225 }
226
227 let param_refs: Vec<&dyn rusqlite::types::ToSql> =
228 params.iter().map(|p| p.as_ref()).collect();
229 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
230 let cards = stmt
231 .query_map(param_refs.as_slice(), Self::row_to_gift_card)
232 .map_err(map_db_error)?
233 .collect::<std::result::Result<Vec<_>, _>>()
234 .map_err(map_db_error)?;
235 Ok(cards)
236 }
237
238 fn charge(
239 &self,
240 id: GiftCardId,
241 amount: Decimal,
242 reference_id: Option<String>,
243 ) -> Result<GiftCardTransaction> {
244 if amount <= Decimal::ZERO {
245 return Err(CommerceError::ValidationError(
246 "Charge amount must be positive".to_string(),
247 ));
248 }
249
250 let id_str = id.to_string();
251 let txn_id = GiftCardTransactionId::new();
252 let txn_id_str = txn_id.to_string();
253 let now = Utc::now();
254 let now_str = now.to_rfc3339();
255
256 with_immediate_transaction(&self.pool, |tx| {
257 let (current_balance_str, status_str, expires_at_raw): (
259 String,
260 String,
261 Option<String>,
262 ) = tx.query_row(
263 "SELECT current_balance, status, expires_at FROM gift_cards WHERE id = ?",
264 [&id_str],
265 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
266 )?;
267
268 let status: GiftCardStatus = parse_enum_row(&status_str, "gift_card", "status")?;
269 if status != GiftCardStatus::Active {
270 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
271 CommerceError::ValidationError("Gift card is not active".to_string()),
272 )));
273 }
274
275 let expires_at = parse_datetime_opt_row(expires_at_raw, "gift_card", "expires_at")?;
276 if expires_at.is_some_and(|exp| exp < now) {
277 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
278 CommerceError::ValidationError("Gift card has expired".to_string()),
279 )));
280 }
281
282 let current_balance =
283 parse_decimal_row(¤t_balance_str, "gift_card", "current_balance")?;
284 if current_balance < amount {
285 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
286 CommerceError::ValidationError("Insufficient gift card balance".to_string()),
287 )));
288 }
289
290 let new_balance = current_balance - amount;
291 let new_status = if new_balance.is_zero() {
292 GiftCardStatus::Depleted
293 } else {
294 GiftCardStatus::Active
295 };
296
297 tx.execute(
298 "UPDATE gift_cards SET current_balance = ?, status = ?, updated_at = ? WHERE id = ?",
299 rusqlite::params![
300 new_balance.to_string(),
301 new_status.to_string(),
302 &now_str,
303 &id_str,
304 ],
305 )?;
306
307 tx.execute(
308 "INSERT INTO gift_card_transactions (id, gift_card_id, amount, balance_after, type, reference_id, created_at)
309 VALUES (?, ?, ?, ?, ?, ?, ?)",
310 rusqlite::params![
311 &txn_id_str,
312 &id_str,
313 amount.to_string(),
314 new_balance.to_string(),
315 "charge",
316 &reference_id,
317 &now_str,
318 ],
319 )?;
320
321 tx.query_row(
322 "SELECT * FROM gift_card_transactions WHERE id = ?",
323 [&txn_id_str],
324 Self::row_to_transaction,
325 )
326 })
327 }
328
329 fn refund(
330 &self,
331 id: GiftCardId,
332 amount: Decimal,
333 reference_id: Option<String>,
334 ) -> Result<GiftCardTransaction> {
335 if amount <= Decimal::ZERO {
336 return Err(CommerceError::ValidationError(
337 "Refund amount must be positive".to_string(),
338 ));
339 }
340
341 let id_str = id.to_string();
342 let txn_id = GiftCardTransactionId::new();
343 let txn_id_str = txn_id.to_string();
344 let now_str = Utc::now().to_rfc3339();
345
346 with_immediate_transaction(&self.pool, |tx| {
347 let (current_balance_str, status_str): (String, String) = tx.query_row(
348 "SELECT current_balance, status FROM gift_cards WHERE id = ?",
349 [&id_str],
350 |row| Ok((row.get(0)?, row.get(1)?)),
351 )?;
352
353 let status: GiftCardStatus = parse_enum_row(&status_str, "gift_card", "status")?;
354 if status == GiftCardStatus::Disabled {
355 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
356 CommerceError::ValidationError(
357 "Cannot refund to a disabled gift card".to_string(),
358 ),
359 )));
360 }
361
362 let current_balance =
363 parse_decimal_row(¤t_balance_str, "gift_card", "current_balance")?;
364 let new_balance = current_balance + amount;
365 let new_status = if status == GiftCardStatus::Expired {
367 GiftCardStatus::Expired
368 } else {
369 GiftCardStatus::Active
370 };
371
372 tx.execute(
373 "UPDATE gift_cards SET current_balance = ?, status = ?, updated_at = ? WHERE id = ?",
374 rusqlite::params![
375 new_balance.to_string(),
376 new_status.to_string(),
377 &now_str,
378 &id_str
379 ],
380 )?;
381
382 tx.execute(
383 "INSERT INTO gift_card_transactions (id, gift_card_id, amount, balance_after, type, reference_id, created_at)
384 VALUES (?, ?, ?, ?, ?, ?, ?)",
385 rusqlite::params![
386 &txn_id_str,
387 &id_str,
388 amount.to_string(),
389 new_balance.to_string(),
390 "refund",
391 &reference_id,
392 &now_str,
393 ],
394 )?;
395
396 tx.query_row(
397 "SELECT * FROM gift_card_transactions WHERE id = ?",
398 [&txn_id_str],
399 Self::row_to_transaction,
400 )
401 })
402 }
403
404 fn disable(&self, id: GiftCardId) -> Result<GiftCard> {
405 let id_str = id.to_string();
406 let now_str = Utc::now().to_rfc3339();
407
408 with_immediate_transaction(&self.pool, |tx| {
409 tx.execute(
410 "UPDATE gift_cards SET status = 'disabled', updated_at = ? WHERE id = ?",
411 rusqlite::params![&now_str, &id_str],
412 )?;
413
414 tx.query_row("SELECT * FROM gift_cards WHERE id = ?", [&id_str], Self::row_to_gift_card)
415 })
416 }
417
418 fn get_transactions(&self, gift_card_id: GiftCardId) -> Result<Vec<GiftCardTransaction>> {
419 let conn = self.conn()?;
420 let mut stmt = conn
421 .prepare(
422 "SELECT * FROM gift_card_transactions WHERE gift_card_id = ? ORDER BY created_at DESC",
423 )
424 .map_err(map_db_error)?;
425 let txns = stmt
426 .query_map([gift_card_id.to_string()], Self::row_to_transaction)
427 .map_err(map_db_error)?
428 .collect::<std::result::Result<Vec<_>, _>>()
429 .map_err(map_db_error)?;
430 Ok(txns)
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::DatabaseConfig;
438 use crate::sqlite::SqliteDatabase;
439 use rust_decimal_macros::dec;
440 use stateset_core::CurrencyCode;
441
442 fn test_repo() -> SqliteGiftCardRepository {
443 let db = SqliteDatabase::new(&DatabaseConfig::in_memory()).unwrap();
444 let conn = db.conn().unwrap();
445 conn.execute_batch(
446 "CREATE TABLE IF NOT EXISTS gift_cards (
447 id TEXT PRIMARY KEY,
448 code TEXT NOT NULL UNIQUE,
449 initial_balance TEXT NOT NULL,
450 current_balance TEXT NOT NULL,
451 currency TEXT NOT NULL DEFAULT 'USD',
452 status TEXT NOT NULL DEFAULT 'active',
453 customer_id TEXT,
454 issued_by TEXT,
455 expires_at TEXT,
456 notes TEXT,
457 created_at TEXT NOT NULL DEFAULT (datetime('now')),
458 updated_at TEXT NOT NULL DEFAULT (datetime('now'))
459 );
460 CREATE TABLE IF NOT EXISTS gift_card_transactions (
461 id TEXT PRIMARY KEY,
462 gift_card_id TEXT NOT NULL,
463 amount TEXT NOT NULL,
464 balance_after TEXT NOT NULL,
465 type TEXT NOT NULL,
466 reference_id TEXT,
467 notes TEXT,
468 created_at TEXT NOT NULL DEFAULT (datetime('now'))
469 );",
470 )
471 .unwrap();
472 SqliteGiftCardRepository::new(db.pool().clone())
473 }
474
475 #[test]
476 fn create_rejects_negative_initial_balance() {
477 let repo = test_repo();
478 let err = repo
479 .create(CreateGiftCard {
480 code: None,
481 initial_balance: dec!(-50.00),
482 currency: CurrencyCode::USD,
483 recipient_email: None,
484 sender_name: None,
485 message: None,
486 expires_at: None,
487 })
488 .expect_err("negative gift card initial balance must be rejected");
489 assert!(matches!(err, CommerceError::ValidationError(_)), "got {err:?}");
490 }
491
492 #[test]
493 fn create_and_get() {
494 let repo = test_repo();
495 let gc = repo
496 .create(CreateGiftCard {
497 code: Some("GIFT-TEST-0001".into()),
498 initial_balance: dec!(50.00),
499 currency: CurrencyCode::USD,
500 recipient_email: Some("alice@example.com".into()),
501 sender_name: Some("Bob".into()),
502 message: Some("Happy birthday!".into()),
503 expires_at: None,
504 })
505 .unwrap();
506
507 assert_eq!(gc.code, "GIFT-TEST-0001");
508 assert_eq!(gc.initial_balance, dec!(50.00));
509 assert_eq!(gc.current_balance, dec!(50.00));
510 assert_eq!(gc.currency, CurrencyCode::USD);
511 assert_eq!(gc.status, GiftCardStatus::Active);
512 assert_eq!(gc.recipient_email.as_deref(), Some("alice@example.com"));
513 assert_eq!(gc.sender_name.as_deref(), Some("Bob"));
514 assert_eq!(gc.message.as_deref(), Some("Happy birthday!"));
515
516 let fetched = repo.get(gc.id).unwrap().unwrap();
517 assert_eq!(fetched.id, gc.id);
518 assert_eq!(fetched.code, gc.code);
519 assert_eq!(fetched.initial_balance, gc.initial_balance);
520 }
521
522 #[test]
523 fn get_by_code() {
524 let repo = test_repo();
525 let gc = repo
526 .create(CreateGiftCard {
527 code: Some("LOOKUP-CODE".into()),
528 initial_balance: dec!(25.00),
529 currency: CurrencyCode::USD,
530 recipient_email: None,
531 sender_name: None,
532 message: None,
533 expires_at: None,
534 })
535 .unwrap();
536
537 let found = repo.get_by_code("LOOKUP-CODE").unwrap().unwrap();
538 assert_eq!(found.id, gc.id);
539 assert_eq!(found.code, "LOOKUP-CODE");
540
541 let missing = repo.get_by_code("NO-SUCH-CODE").unwrap();
542 assert!(missing.is_none());
543 }
544
545 fn create_card(repo: &SqliteGiftCardRepository, code: &str, balance: Decimal) -> GiftCard {
546 repo.create(CreateGiftCard {
547 code: Some(code.into()),
548 initial_balance: balance,
549 currency: CurrencyCode::USD,
550 recipient_email: None,
551 sender_name: None,
552 message: None,
553 expires_at: None,
554 })
555 .unwrap()
556 }
557
558 #[test]
559 fn charge_rejects_nonpositive_amount() {
560 let repo = test_repo();
561 let gc = create_card(&repo, "CHARGE-NONPOS", dec!(50.00));
562
563 assert!(repo.charge(gc.id, Decimal::ZERO, None).is_err());
564 assert!(repo.charge(gc.id, dec!(-10.00), None).is_err());
565
566 let fetched = repo.get(gc.id).unwrap().unwrap();
567 assert_eq!(fetched.current_balance, dec!(50.00));
568 assert!(repo.get_transactions(gc.id).unwrap().is_empty());
569 }
570
571 #[test]
572 fn charge_rejects_date_expired_card() {
573 let repo = test_repo();
574 let gc = repo
575 .create(CreateGiftCard {
576 code: Some("CHARGE-EXPIRED".into()),
577 initial_balance: dec!(50.00),
578 currency: CurrencyCode::USD,
579 recipient_email: None,
580 sender_name: None,
581 message: None,
582 expires_at: Some(Utc::now() - chrono::Duration::days(1)),
583 })
584 .unwrap();
585
586 assert_eq!(gc.status, GiftCardStatus::Active);
588 assert!(repo.charge(gc.id, dec!(10.00), None).is_err());
589
590 let fetched = repo.get(gc.id).unwrap().unwrap();
591 assert_eq!(fetched.current_balance, dec!(50.00));
592 }
593
594 #[test]
595 fn refund_rejects_nonpositive_amount() {
596 let repo = test_repo();
597 let gc = create_card(&repo, "REFUND-NONPOS", dec!(50.00));
598
599 assert!(repo.refund(gc.id, Decimal::ZERO, None).is_err());
600 assert!(repo.refund(gc.id, dec!(-10.00), None).is_err());
601
602 let fetched = repo.get(gc.id).unwrap().unwrap();
603 assert_eq!(fetched.current_balance, dec!(50.00));
604 }
605
606 #[test]
607 fn refund_rejects_disabled_card() {
608 let repo = test_repo();
609 let gc = create_card(&repo, "REFUND-DISABLED", dec!(50.00));
610 repo.disable(gc.id).unwrap();
611
612 assert!(repo.refund(gc.id, dec!(10.00), None).is_err());
613
614 let fetched = repo.get(gc.id).unwrap().unwrap();
615 assert_eq!(fetched.status, GiftCardStatus::Disabled);
616 assert_eq!(fetched.current_balance, dec!(50.00));
617 }
618
619 #[test]
620 fn concurrent_charges_cannot_overspend() {
621 use std::sync::{Arc, Barrier};
622 use std::thread;
623
624 let db = Arc::new(SqliteDatabase::new(&DatabaseConfig::in_memory()).unwrap());
625 let repo = SqliteGiftCardRepository::new(db.pool().clone());
626 let gc = create_card(&repo, "CHARGE-RACE", dec!(50.00));
627
628 let thread_count = 10;
629 let barrier = Arc::new(Barrier::new(thread_count));
630 let mut handles = Vec::new();
631 for _ in 0..thread_count {
632 let db = Arc::clone(&db);
633 let barrier = Arc::clone(&barrier);
634 let card_id = gc.id;
635 handles.push(thread::spawn(move || {
636 let repo = SqliteGiftCardRepository::new(db.pool().clone());
637 barrier.wait();
638 repo.charge(card_id, dec!(30.00), None)
639 }));
640 }
641
642 let results: Vec<_> = handles.into_iter().map(|h| h.join().expect("thread")).collect();
643 let successes = results.iter().filter(|r| r.is_ok()).count();
644 assert!(successes <= 1, "gift card overspent under concurrency: {results:?}");
650
651 let fetched = repo.get(gc.id).unwrap().unwrap();
652 assert_eq!(
653 fetched.current_balance,
654 dec!(50.00) - dec!(30.00) * Decimal::from(successes as u64),
655 "balance must reflect exactly the successful charge: {results:?}"
656 );
657 assert_eq!(repo.get_transactions(gc.id).unwrap().len(), successes);
658 }
659
660 #[test]
661 fn charge_then_refund_roundtrip() {
662 let repo = test_repo();
663 let gc = create_card(&repo, "CHARGE-REFUND-RT", dec!(50.00));
664
665 let charge = repo.charge(gc.id, dec!(50.00), Some("ORD-1".into())).unwrap();
666 assert_eq!(charge.balance_after, Decimal::ZERO);
667 assert_eq!(repo.get(gc.id).unwrap().unwrap().status, GiftCardStatus::Depleted);
668
669 let refund = repo.refund(gc.id, dec!(20.00), Some("ORD-1".into())).unwrap();
671 assert_eq!(refund.balance_after, dec!(20.00));
672 let fetched = repo.get(gc.id).unwrap().unwrap();
673 assert_eq!(fetched.status, GiftCardStatus::Active);
674 assert_eq!(fetched.current_balance, dec!(20.00));
675 }
676
677 #[test]
678 fn disable() {
679 let repo = test_repo();
680 let gc = repo
681 .create(CreateGiftCard {
682 code: Some("DISABLE-ME".into()),
683 initial_balance: dec!(100.00),
684 currency: CurrencyCode::USD,
685 recipient_email: None,
686 sender_name: None,
687 message: None,
688 expires_at: None,
689 })
690 .unwrap();
691
692 assert_eq!(gc.status, GiftCardStatus::Active);
693
694 let disabled = repo.disable(gc.id).unwrap();
695 assert_eq!(disabled.status, GiftCardStatus::Disabled);
696 assert_eq!(disabled.id, gc.id);
697
698 let fetched = repo.get(gc.id).unwrap().unwrap();
699 assert_eq!(fetched.status, GiftCardStatus::Disabled);
700 }
701}