Files
vmall/apps/api/tests/orders.rs
T

319 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}