319 lines
10 KiB
Rust
319 lines
10 KiB
Rust
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 (_, _, _, sku_a) = setup_sellable(&app, &admin, "split-a", 1000, 5).await;
|
||
let (_, _, _, 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;
|
||
|
||
let orders = checkout(&app, &buyer).await;
|
||
assert_eq!(orders.len(), 2, "one order per shop");
|
||
let totals: Vec<i64> = 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::<serde_json::Value>().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::<serde_json::Value>().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::<serde_json::Value>().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::<serde_json::Value>().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::<serde_json::Value>().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);
|
||
}
|