mod common; use common::{ add_to_cart, checkout, client, login_admin, pay, register_customer, setup_sellable, spawn_app, }; use serial_test::serial; async fn stock_of(app: &common::TestApp, sku_id: &str) -> i32 { sqlx::query_scalar("SELECT stock FROM skus WHERE id = $1::uuid") .bind(sku_id) .fetch_one(&app.db) .await .unwrap() } #[tokio::test] #[serial] async fn checkout_splits_orders_per_shop_and_clears_cart() { let app = spawn_app().await; let admin = login_admin(&app).await; let (_, shop_a, _, sku_a) = setup_sellable(&app, &admin, "split-a", 1000, 5).await; let (_, shop_b, _, sku_b) = setup_sellable(&app, &admin, "split-b", 2000, 5).await; let (buyer, _) = register_customer(&app, "buyer").await; add_to_cart(&app, &buyer, &sku_a, 2).await; add_to_cart(&app, &buyer, &sku_b, 1).await; // The cart view must carry the owning shop and current stock, so the // storefront can group lines and cap quantity without the mock catalog. let res = client() .get(app.url("/api/cart")) .bearer_auth(&buyer) .send() .await .unwrap(); assert_eq!(res.status(), 200); let view: serde_json::Value = res.json().await.unwrap(); let items = view["items"].as_array().unwrap(); assert_eq!(items.len(), 2); let shop_ids: std::collections::HashSet = items .iter() .map(|i| i["shop_id"].as_str().unwrap().to_string()) .collect(); assert_eq!(shop_ids.len(), 2, "each line reports its own shop"); assert!(shop_ids.contains(&shop_a) && shop_ids.contains(&shop_b)); for item in items { assert!( item["shop_name"]["en"].as_str().is_some_and(|s| !s.is_empty()), "line must carry a bilingual shop name" ); assert_eq!(item["stock"], 5, "stock is the SKU's, before checkout decrements it"); } let orders = checkout(&app, &buyer).await; assert_eq!(orders.len(), 2, "one order per shop"); let totals: Vec = orders .iter() .map(|o| o["total_minor"].as_i64().unwrap()) .collect(); assert!(totals.contains(&2000) && totals.contains(&2000)); // 2×1000 and 1×2000 assert!(orders.iter().all(|o| o["status"] == "pending_payment")); // stock decremented assert_eq!(stock_of(&app, &sku_a).await, 3); assert_eq!(stock_of(&app, &sku_b).await, 4); // cart cleared let res = client() .get(app.url("/api/cart")) .bearer_auth(&buyer) .send() .await .unwrap(); assert_eq!(res.json::().await.unwrap()["items"] .as_array() .unwrap() .len(), 0); } #[tokio::test] #[serial] async fn checkout_insufficient_stock_rolls_back() { let app = spawn_app().await; let admin = login_admin(&app).await; let (_, _, _, sku) = setup_sellable(&app, &admin, "lowstock", 500, 1).await; let (buyer, _) = register_customer(&app, "buyer2").await; add_to_cart(&app, &buyer, &sku, 2).await; let res = client() .post(app.url("/api/orders/checkout")) .bearer_auth(&buyer) .json(&serde_json::json!({ "shipping_address": { "recipient": "R", "phone": "1", "country": "US", "region": "CA", "city": "SJ", "line1": "1 Way", "postal_code": "95131" }, "currency": "USD" })) .send() .await .unwrap(); assert_eq!(res.status(), 409); assert_eq!(stock_of(&app, &sku).await, 1, "stock unchanged"); } #[tokio::test] #[serial] async fn cancel_rules_and_stock_restore() { let app = spawn_app().await; let admin = login_admin(&app).await; let (_, _, _, sku) = setup_sellable(&app, &admin, "cancelme", 800, 4).await; let (buyer, _) = register_customer(&app, "buyer3").await; add_to_cart(&app, &buyer, &sku, 2).await; let orders = checkout(&app, &buyer).await; let order_id = orders[0]["id"].as_str().unwrap(); assert_eq!(stock_of(&app, &sku).await, 2); let res = client() .post(app.url(&format!("/api/orders/{order_id}/cancel"))) .bearer_auth(&buyer) .send() .await .unwrap(); assert_eq!(res.status(), 200); assert_eq!(stock_of(&app, &sku).await, 4, "stock restored"); // paid orders cannot be cancelled add_to_cart(&app, &buyer, &sku, 1).await; let orders = checkout(&app, &buyer).await; let order_id = orders[0]["id"].as_str().unwrap(); pay(&app, &buyer, order_id).await; let res = client() .post(app.url(&format!("/api/orders/{order_id}/cancel"))) .bearer_auth(&buyer) .send() .await .unwrap(); assert_eq!(res.status(), 409); } #[tokio::test] #[serial] async fn fulfillment_flow_partial_then_complete() { let app = spawn_app().await; let admin = login_admin(&app).await; let (owner, _, _, sku) = setup_sellable(&app, &admin, "fulfil", 1500, 10).await; let (buyer, _) = register_customer(&app, "buyer4").await; add_to_cart(&app, &buyer, &sku, 3).await; let orders = checkout(&app, &buyer).await; let order = &orders[0]; let order_id = order["id"].as_str().unwrap(); let item_id = order["items"][0]["id"].as_str().unwrap(); pay(&app, &buyer, order_id).await; // over-shipping the remainder is rejected let res = client() .post(app.url(&format!("/api/shop/orders/{order_id}/shipments"))) .bearer_auth(&owner) .json(&serde_json::json!({ "carrier": "UPS", "tracking_no": "T1", "items": [{ "order_item_id": item_id, "qty": 4 }] })) .send() .await .unwrap(); assert_eq!(res.status(), 400); // partial shipment of 2 → order fulfilling let res = client() .post(app.url(&format!("/api/shop/orders/{order_id}/shipments"))) .bearer_auth(&owner) .json(&serde_json::json!({ "carrier": "UPS", "tracking_no": "T1", "items": [{ "order_item_id": item_id, "qty": 2 }] })) .send() .await .unwrap(); assert_eq!(res.status(), 201); let shipment1: serde_json::Value = res.json().await.unwrap(); let shipment1_id = shipment1["id"].as_str().unwrap().to_string(); let res = client() .post(app.url(&format!("/api/shop/shipments/{shipment1_id}/ship"))) .bearer_auth(&owner) .send() .await .unwrap(); assert_eq!(res.status(), 200); let res = client() .get(app.url(&format!("/api/orders/{order_id}"))) .bearer_auth(&buyer) .send() .await .unwrap(); assert_eq!(res.json::().await.unwrap()["status"], "fulfilling"); // second shipment covers remainder → shipped after mark let res = client() .post(app.url(&format!("/api/shop/orders/{order_id}/shipments"))) .bearer_auth(&owner) .json(&serde_json::json!({ "carrier": "UPS", "tracking_no": "T2", "items": [{ "order_item_id": item_id, "qty": 1 }] })) .send() .await .unwrap(); let shipment2_id = res.json::().await.unwrap()["id"] .as_str() .unwrap() .to_string(); client() .post(app.url(&format!("/api/shop/shipments/{shipment2_id}/ship"))) .bearer_auth(&owner) .send() .await .unwrap(); let res = client() .get(app.url(&format!("/api/orders/{order_id}"))) .bearer_auth(&buyer) .send() .await .unwrap(); assert_eq!(res.json::().await.unwrap()["status"], "shipped"); // confirm both deliveries → completed for sid in [&shipment1_id, &shipment2_id] { let res = client() .post(app.url(&format!("/api/shipments/{sid}/confirm-delivered"))) .bearer_auth(&buyer) .send() .await .unwrap(); assert_eq!(res.status(), 200); } let res = client() .get(app.url(&format!("/api/orders/{order_id}"))) .bearer_auth(&buyer) .send() .await .unwrap(); assert_eq!(res.json::().await.unwrap()["status"], "completed"); } #[tokio::test] #[serial] async fn invoice_lifecycle() { let app = spawn_app().await; let admin = login_admin(&app).await; let (owner, _, _, sku) = setup_sellable(&app, &admin, "invc", 3000, 2).await; let (buyer, _) = register_customer(&app, "buyer5").await; add_to_cart(&app, &buyer, &sku, 1).await; let orders = checkout(&app, &buyer).await; let order_id = orders[0]["id"].as_str().unwrap(); // pending_payment orders cannot be invoiced let res = client() .post(app.url(&format!("/api/orders/{order_id}/invoice"))) .bearer_auth(&buyer) .json(&serde_json::json!({ "title": "Me", "kind": "personal" })) .send() .await .unwrap(); assert_eq!(res.status(), 400); pay(&app, &buyer, order_id).await; // company invoice requires tax_no let res = client() .post(app.url(&format!("/api/orders/{order_id}/invoice"))) .bearer_auth(&buyer) .json(&serde_json::json!({ "title": "ACME", "kind": "company" })) .send() .await .unwrap(); assert_eq!(res.status(), 400); let res = client() .post(app.url(&format!("/api/orders/{order_id}/invoice"))) .bearer_auth(&buyer) .json(&serde_json::json!({ "title": "ACME", "tax_no": "US-123", "kind": "company" })) .send() .await .unwrap(); assert_eq!(res.status(), 201); let invoice: serde_json::Value = res.json().await.unwrap(); assert_eq!(invoice["status"], "requested"); assert_eq!(invoice["amount_minor"], 3000); // duplicate rejected let res = client() .post(app.url(&format!("/api/orders/{order_id}/invoice"))) .bearer_auth(&buyer) .json(&serde_json::json!({ "title": "Again", "kind": "personal" })) .send() .await .unwrap(); assert_eq!(res.status(), 409); // shop issues it let invoice_id = invoice["id"].as_str().unwrap(); let res = client() .post(app.url(&format!("/api/shop/invoices/{invoice_id}/issue"))) .bearer_auth(&owner) .send() .await .unwrap(); assert_eq!(res.status(), 200); let issued: serde_json::Value = res.json().await.unwrap(); assert_eq!(issued["status"], "issued"); assert!(issued["invoice_no"].as_str().unwrap().starts_with("INV")); // customer sees issued invoice let res = client() .get(app.url("/api/invoices")) .bearer_auth(&buyer) .send() .await .unwrap(); let invoices: serde_json::Value = res.json().await.unwrap(); assert_eq!(invoices[0]["status"], "issued"); } #[tokio::test] #[serial] async fn order_ownership_enforced() { let app = spawn_app().await; let admin = login_admin(&app).await; let (_, _, _, sku) = setup_sellable(&app, &admin, "ownerchk", 100, 1).await; let (buyer, _) = register_customer(&app, "buyer6").await; let (other, _) = register_customer(&app, "buyer7").await; add_to_cart(&app, &buyer, &sku, 1).await; let orders = checkout(&app, &buyer).await; let order_id = orders[0]["id"].as_str().unwrap(); let res = client() .get(app.url(&format!("/api/orders/{order_id}"))) .bearer_auth(&other) .send() .await .unwrap(); assert_eq!(res.status(), 404); }