Tests and coverage by phase
Markers distinguish covered lines, uncovered lines, and lines with no coverage measurement. Each measured run is shown with the lines above and below it.
shop / shop-rs-api / integration
probe-bind.stderr adapter.stdout tests.stdout adapter.stderr api.stderr probe-missing-database.stderr build.log- Passed shop_project_cart_suite_adds_line_to_cart_test (0 ms)
- Passed shop_project_cart_suite_rejects_quantity_beyond_stock_test (0 ms)
- Passed shop_project_cart_suite_rejects_zero_quantity_test (0 ms)
- Passed shop_project_cart_suite_unknown_cart_not_found_test (0 ms)
- Passed shop_project_catalog_suite_lists_seeded_products_test (0 ms)
- Passed shop_project_catalog_suite_rejects_invalid_sku_test (0 ms)
- Passed shop_project_catalog_suite_serves_openapi_test (0 ms)
- Passed shop_project_catalog_suite_unknown_sku_not_found_test (0 ms)
- Passed shop_project_checkout_suite_places_order_and_decrements_stock_test (0 ms)
- Passed shop_project_checkout_suite_rejects_empty_cart_test (0 ms)
- Passed shop_project_checkout_suite_rejects_second_checkout_test (0 ms)
shop-rs-api/src/application/service.rs: 55/60 covered lines
runoldnew±codecov
1212}
1313
1414impl From<RepositoryError> for ShopError {
1515 fn from(error: RepositoryError) -> Self {
1616 match error {
1717 RepositoryError::Rejected(domain) => Self::Domain(domain),
1818 RepositoryError::Unavailable(reason) => Self::Unavailable(reason),
1919 }
2020 }
2121}
2222
2323pub struct CatalogService {
…
2525}
2626
2828 pub fn new(products: Arc<dyn ProductRepository>) -> Self {
2929 Self { products }
3030 }
3131 pub async fn list_products(&self) -> Result<Vec<Product>, ShopError> {
3333 }
3535 self.products
3737 .await?
3838 .ok_or_else(|| ShopError::Domain(DomainError::ProductNotFound(sku.clone())))
3939 }
4040}
4141
4242pub struct CartService {
…
4646}
4747
4949 pub fn new(
5050 products: Arc<dyn ProductRepository>,
5151 carts: Arc<dyn CartRepository>,
5252 orders: Arc<dyn OrderRepository>,
5353 ) -> Self {
5454 Self {
5555 products,
5656 carts,
5757 orders,
5858 }
5959 }
6060 pub async fn open_cart(&self) -> Result<Cart, ShopError> {
6262 }
6464 self.carts
6666 .await?
6767 .ok_or_else(|| ShopError::Domain(DomainError::CartNotFound(cart_id.clone())))
6868 }
6969 pub async fn add_to_cart(
7070 &self,
7474 ) -> Result<Cart, ShopError> {
7676 let product = self
7777 .products
7979 .await?
8080 .ok_or_else(|| ShopError::Domain(DomainError::ProductNotFound(sku.clone())))?;
8484 }
9191 }
9292}
shop-rs-api/src/domain/model.rs: 133/138 covered lines
runoldnew±codecov
2121}
2222
2424 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2525 match self {
2626 Self::InvalidSku => write!(
2727 formatter,
2828 "sku must be {MINIMUM_SKU_LENGTH}-{MAXIMUM_SKU_LENGTH} uppercase letters, digits or dashes"
2929 ),
3030 Self::InvalidQuantity => {
3131 write!(formatter, "quantity must be between 1 and {MAXIMUM_LINE_QUANTITY}")
3232 }
3333 Self::InvalidMoney => write!(formatter, "amount must not be negative"),
3434 Self::InvalidCartId => write!(formatter, "cart id must be a UUID"),
3535 Self::AmountOverflow => write!(formatter, "order total exceeds the supported range"),
3636 Self::ProductNotFound(sku) => write!(formatter, "product {sku} does not exist"),
3737 Self::CartNotFound(cart_id) => write!(formatter, "cart {cart_id} does not exist"),
3838 Self::CartClosed => write!(formatter, "cart is already checked out"),
3939 Self::EmptyCart => write!(formatter, "cart has no items"),
4040 Self::OutOfStock(sku) => write!(formatter, "not enough stock for {sku}"),
4141 }
4242 }
4343}
4444
4545impl std::error::Error for DomainError {}
…
4848pub struct Money(i64);
4949
5353 return Err(DomainError::InvalidMoney);
5454 }
5656 }
5757 pub fn zero() -> Self {
5858 Self(0)
5959 }
6060 pub fn cents(self) -> i64 {
6161 self.0
6262 }
6464 self.0
6666 .map(Self)
6767 .ok_or(DomainError::AmountOverflow)
6868 }
7070 self.0
7171 .checked_add(other.0)
7272 .map(Self)
7373 .ok_or(DomainError::AmountOverflow)
7474 }
7575}
7676
7777#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
7878pub struct Sku(String);
7979
8181 pub fn parse(raw: &str) -> Result<Self, DomainError> {
8282 let length_valid = (MINIMUM_SKU_LENGTH..=MAXIMUM_SKU_LENGTH).contains(&raw.len());
8383 let characters_valid = raw.chars().all(|character| {
8484 character.is_ascii_uppercase() || character.is_ascii_digit() || character == '-'
8585 });
8686 if !length_valid || !characters_valid {
8787 return Err(DomainError::InvalidSku);
8888 }
8989 Ok(Self(raw.to_owned()))
9090 }
9191 pub fn as_str(&self) -> &str {
9292 &self.0
9393 }
9494}
9595
9797 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
9898 formatter.write_str(&self.0)
9999 }
100100}
101101
102102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103103pub struct Quantity(u32);
104104
108108 .ok()
109109 .filter(|candidate| (1..=MAXIMUM_LINE_QUANTITY).contains(candidate))
110110 .map(Self)
111111 .ok_or(DomainError::InvalidQuantity)
112112 }
113113 pub fn value(self) -> u32 {
114114 self.0
115115 }
116116}
117117
118118#[derive(Debug, Clone, PartialEq, Eq)]
119119pub struct CartId(String);
120120
122122 pub fn parse(raw: &str) -> Result<Self, DomainError> {
123123 let shape_valid = raw.len() == CART_ID_LENGTH
124124 && raw.char_indices().all(|(position, character)| {
125125 if CART_ID_DASH_POSITIONS.contains(&position) {
126126 character == '-'
127127 } else {
128128 character.is_ascii_hexdigit()
129129 }
130130 });
131131 if !shape_valid {
132132 return Err(DomainError::InvalidCartId);
133133 }
134134 Ok(Self(raw.to_ascii_lowercase()))
135135 }
136136 pub fn as_str(&self) -> &str {
137137 &self.0
138138 }
139139}
140140
142142 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
143143 formatter.write_str(&self.0)
144144 }
145145}
146146
147147#[derive(Debug, Clone, PartialEq, Eq)]
…
153153}
154154
155155impl Product {
158158 return Err(DomainError::OutOfStock(self.sku.clone()));
159159 }
160160 Ok(())
161161 }
162162}
163163
164164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
…
203203}
204204
205205impl Cart {
208208 let existing = self
209209 .lines
210210 .iter()
216216 sku: product.sku.clone(),
217217 quantity: merged,
218218 })
219219 }
220220 pub fn checkout(&self, products: &[Product]) -> Result<OrderDraft, DomainError> {
222222 if self.lines.is_empty() {
223223 return Err(DomainError::EmptyCart);
224224 }
225225 let lines = self
226226 .lines
227227 .iter()
229229 .collect::<Result<Vec<_>, _>>()?;
232232 })?;
233233 Ok(OrderDraft {
234234 cart_id: self.id.clone(),
235235 lines,
236236 total,
237237 })
238238 }
239239 fn ensure_open(&self) -> Result<(), DomainError> {
240240 match self.status {
241241 CartStatus::Open => Ok(()),
242242 CartStatus::CheckedOut => Err(DomainError::CartClosed),
243243 }
244244 }
246246 let product = products
247247 .iter()
249249 .ok_or_else(|| DomainError::ProductNotFound(line.sku.clone()))?;
251251 Ok(OrderLine {
252252 sku: line.sku.clone(),
253253 quantity: line.quantity,
254254 unit_price: product.price,
255255 })
256256 }
257257}
258258
259259#[cfg(test)]
shop-rs-api/src/domain/port.rs: 0/5 covered lines
runoldnew±codecov
1111}
1212
1414 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1515 match self {
1616 Self::Rejected(error) => write!(formatter, "{error}"),
1717 Self::Unavailable(reason) => write!(formatter, "storage unavailable: {reason}"),
1818 }
1919 }
2020}
2121
2222impl std::error::Error for RepositoryError {}
shop-rs-api/src/infrastructure/postgres.rs: 82/94 covered lines
runoldnew±codecov
1212const CART_STATUS_OPEN: &str = "open";
1313const CART_STATUS_CHECKED_OUT: &str = "checked_out";
1414
1515pub async fn connect(database_url: &str) -> Result<PgPool, RepositoryError> {
1818 .max_connections(MAXIMUM_POOL_CONNECTIONS)
2020 .await,
2121 "connect",
2222 )?;
2424 Ok(pool)
2525}
2626
2727fn unavailable(operation: &'static str, error: &dyn std::error::Error) -> RepositoryError {
2828 tracing::error!(upstream = "postgres", operation, error = %error, "storage operation failed");
2929 RepositoryError::Unavailable(operation.to_owned())
3030}
3131
3232fn checked<T, E>(result: Result<T, E>, operation: &'static str) -> Result<T, RepositoryError>
3333where
3434 E: std::error::Error,
3535{
3737}
3838
3939fn product_from_row(row: &PgRow) -> Result<Product, RepositoryError> {
4545}
4646
4747fn product_from_parts(
4848 sku: &str,
4949 name: String,
5050 price_cents: i64,
5151 stock: i32,
5252) -> Result<Product, RepositoryError> {
5353 Ok(Product {
5555 name,
5757 stock: u32::try_from(stock)
5858 .map_err(|_| RepositoryError::Rejected(DomainError::InvalidQuantity))?,
5959 })
6060}
6161
6262pub struct PostgresProductRepository {
6363 pool: PgPool,
6464}
6565
6767 pub fn new(pool: PgPool) -> Self {
6868 Self { pool }
6969 }
7070}
7171
7272#[async_trait]
7474 async fn list(&self) -> Result<Vec<Product>, RepositoryError> {
7676 sqlx::query(r#"SELECT SKU, NAME, PRICE_CENTS, STOCK FROM product ORDER BY SKU"#)
7777 .fetch_all(&self.pool)
…
7979 "list_products",
8080 )?;
8282 }
8585 sqlx::query(r#"SELECT SKU, NAME, PRICE_CENTS, STOCK FROM product WHERE SKU = $1"#)
8686 .bind(sku.as_str())
…
8989 "find_product",
9090 )?;
9292 }
9494 let values: Vec<String> = skus.iter().map(|sku| sku.as_str().to_owned()).collect();
9696 sqlx::query(r#"SELECT SKU, NAME, PRICE_CENTS, STOCK FROM product WHERE SKU = ANY($1)"#)
9797 .bind(&values)
…
100100 "find_products",
101101 )?;
103103 }
104104}
105105
106106pub struct PostgresCartRepository {
…
108108}
109109
111111 pub fn new(pool: PgPool) -> Self {
112112 Self { pool }
113113 }
114114}
115115
116116fn cart_status(raw: &str) -> Result<CartStatus, RepositoryError> {
117117 match raw {
120120 other => {
121121 tracing::error!(upstream = "postgres", status = other, "unknown cart status");
122122 Err(RepositoryError::Unavailable("decode_cart".to_owned()))
123123 }
124124 }
125125}
126126
131131}
132132
137137 })
138138}
139139
140140fn quantity_column(quantity: u32) -> Result<i32, RepositoryError> {
141141 i32::try_from(quantity).map_err(|_| RepositoryError::Rejected(DomainError::InvalidQuantity))
142142}
143143
144144fn close_outcome(rows_affected: u64) -> Result<(), RepositoryError> {
145145 if rows_affected == 0 {
146146 Err(RepositoryError::Rejected(DomainError::CartClosed))
147147 } else {
148148 Ok(())
149149 }
150150}
151151
153153 if rows_affected == 0 {
154154 Err(RepositoryError::Rejected(DomainError::OutOfStock(
155155 sku.clone(),
156156 )))
157157 } else {
158158 Ok(())
159159 }
160160}
161161
162162#[async_trait]
164164 async fn create(&self) -> Result<Cart, RepositoryError> {
166166 sqlx::query(r#"INSERT INTO cart (STATUS) VALUES ($1) RETURNING ID::text AS id"#)
…
175175 status: CartStatus::Open,
177177 })
178178 }
181181 sqlx::query(r#"SELECT STATUS FROM cart WHERE ID = $1::uuid"#)
182182 .bind(cart_id.as_str())
…
206206 .collect::<Result<_, _>>()?,
207207 }))
208208 }
209209 async fn save_line(&self, cart_id: &CartId, line: &CartLine) -> Result<(), RepositoryError> {
212212 sqlx::query(
…
221221 "save_cart_line",
222222 )?;
223223 Ok(())
224224 }
225225}
226226
227227pub struct PostgresOrderRepository {
…
229229}
230230
232232 pub fn new(pool: PgPool) -> Self {
233233 Self { pool }
234234 }
235235}
236236
237237#[async_trait]
239239 async fn place(&self, draft: &OrderDraft) -> Result<Order, RepositoryError> {
242242 sqlx::query(r#"UPDATE cart SET STATUS = $2 WHERE ID = $1::uuid AND STATUS = $3"#)
…
293293 lines: draft.lines.clone(),
294294 total: draft.total,
295295 })
296296 }
297297}
298298
299299#[cfg(test)]
shop-rs-api/src/infrastructure/web.rs: 148/165 covered lines
runoldnew±codecov
2929}
3030
3131impl From<Product> for ProductResponse {
3232 fn from(product: Product) -> Self {
3333 Self {
3434 sku: product.sku.as_str().to_owned(),
3535 name: product.name,
3636 price_cents: product.price.cents(),
3737 stock: product.stock,
3838 }
3939 }
4040}
4141
4242#[derive(Debug, Serialize, ToSchema)]
…
5252}
5353
5656 Self {
5757 sku: line.sku.as_str().to_owned(),
5959 }
6060 }
6161}
6262
6363#[derive(Debug, Serialize, ToSchema)]
…
6868}
6969
7070impl From<CartStatus> for CartStatusResponse {
7171 fn from(status: CartStatus) -> Self {
7272 match status {
7373 CartStatus::Open => Self::Open,
7474 CartStatus::CheckedOut => Self::CheckedOut,
7575 }
7676 }
7777}
7878
7979#[derive(Debug, Serialize, ToSchema)]
…
8484}
8585
8686impl From<Cart> for CartResponse {
8787 fn from(cart: Cart) -> Self {
8888 Self {
8989 id: cart.id.as_str().to_owned(),
9090 status: cart.status.into(),
9292 }
9393 }
9494}
9595
9696#[derive(Debug, Serialize, ToSchema)]
…
101101}
102102
103103impl From<OrderLine> for OrderLineResponse {
104104 fn from(line: OrderLine) -> Self {
105105 Self {
106106 sku: line.sku.as_str().to_owned(),
108108 unit_price_cents: line.unit_price.cents(),
109109 }
110110 }
111111}
112112
113113#[derive(Debug, Serialize, ToSchema)]
…
119119}
120120
121121impl From<Order> for OrderResponse {
122122 fn from(order: Order) -> Self {
123123 Self {
124124 id: order.id,
125125 cart_id: order.cart_id.as_str().to_owned(),
126126 lines: order
127127 .lines
128128 .into_iter()
130130 .collect(),
131131 total_cents: order.total.cents(),
132132 }
133133 }
134134}
135135
136136#[derive(Debug, Deserialize, ToSchema)]
…
158158}
159159
160160impl From<DomainError> for ApiError {
161161 fn from(error: DomainError) -> Self {
162162 let (status, code) = match &error {
163163 DomainError::InvalidSku => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_sku"),
164164 DomainError::InvalidQuantity => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_quantity"),
165165 DomainError::InvalidMoney => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_money"),
166166 DomainError::InvalidCartId => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_cart_id"),
167167 DomainError::AmountOverflow => (StatusCode::UNPROCESSABLE_ENTITY, "amount_overflow"),
168168 DomainError::EmptyCart => (StatusCode::UNPROCESSABLE_ENTITY, "empty_cart"),
169169 DomainError::ProductNotFound(_) => (StatusCode::NOT_FOUND, "product_not_found"),
170170 DomainError::CartNotFound(_) => (StatusCode::NOT_FOUND, "cart_not_found"),
171171 DomainError::CartClosed => (StatusCode::CONFLICT, "cart_closed"),
172172 DomainError::OutOfStock(_) => (StatusCode::CONFLICT, "out_of_stock"),
173173 };
174174 Self {
175175 status,
176176 code,
177177 message: error.to_string(),
178178 }
179179 }
180180}
181181
182182impl From<ShopError> for ApiError {
183183 fn from(error: ShopError) -> Self {
184184 match error {
185185 ShopError::Domain(domain_error) => domain_error.into(),
186186 ShopError::Unavailable(operation) => {
187187 tracing::error!(operation = %operation, "request failed on storage");
188188 Self {
189189 status: StatusCode::INTERNAL_SERVER_ERROR,
190190 code: "storage_unavailable",
191191 message: "storage is unavailable".to_owned(),
192192 }
193193 }
194194 }
195195 }
196196}
197197
198198impl From<JsonRejection> for ApiError {
199199 fn from(rejection: JsonRejection) -> Self {
200200 Self {
201201 status: StatusCode::UNPROCESSABLE_ENTITY,
202202 code: "invalid_body",
203203 message: rejection.body_text(),
204204 }
205205 }
206206}
207207
208208impl IntoResponse for ApiError {
209209 fn into_response(self) -> Response {
210210 let body = ErrorResponse {
211211 error: ErrorBody {
212212 code: self.code,
213213 message: self.message,
214214 },
215215 };
217217 }
218218}
219219
220220#[utoipa::path(get, path = "/health", tag = "Health", responses((status = 200, description = "Service is up")))]
221221async fn health() -> StatusCode {
222222 StatusCode::OK
223223}
224224
225225#[utoipa::path(get, path = "/v1/products", tag = "Catalog",
226226 responses((status = 200, body = ProductListResponse), (status = 500, body = ErrorResponse)))]
227227async fn list_products(
228228 State(state): State<AppState>,
229229) -> Result<Json<ProductListResponse>, ApiError> {
230230 let items: Vec<ProductResponse> = state
231231 .catalog
233233 .await?
234234 .into_iter()
236236 .collect();
237237 let total = items.len();
238238 Ok(Json(ProductListResponse { items, total }))
239239}
240240
241241#[utoipa::path(get, path = "/v1/products/{sku}", tag = "Catalog",
242242 params(("sku" = String, Path, description = "Product SKU, e.g. MUG-STONE")),
243243 responses((status = 200, body = ProductResponse), (status = 404, body = ErrorResponse), (status = 422, body = ErrorResponse)))]
244244async fn find_product(
245245 State(state): State<AppState>,
246246 Path(sku): Path<String>,
247247) -> Result<Json<ProductResponse>, ApiError> {
250250}
251251
252252#[utoipa::path(post, path = "/v1/carts", tag = "Cart",
253253 responses((status = 201, body = CartResponse), (status = 500, body = ErrorResponse)))]
254254async fn open_cart(
255255 State(state): State<AppState>,
256256) -> Result<(StatusCode, Json<CartResponse>), ApiError> {
258258 Ok((StatusCode::CREATED, Json(cart.into())))
259259}
260260
261261#[utoipa::path(get, path = "/v1/carts/{id}", tag = "Cart",
262262 params(("id" = String, Path, description = "Cart UUID")),
263263 responses((status = 200, body = CartResponse), (status = 404, body = ErrorResponse), (status = 422, body = ErrorResponse)))]
264264async fn find_cart(
265265 State(state): State<AppState>,
266266 Path(cart_id): Path<String>,
267267) -> Result<Json<CartResponse>, ApiError> {
270270}
271271
272272#[utoipa::path(post, path = "/v1/carts/{id}/items", tag = "Cart",
273273 params(("id" = String, Path, description = "Cart UUID")),
274274 request_body = AddCartItemRequest,
275275 responses((status = 200, body = CartResponse), (status = 404, body = ErrorResponse),
276276 (status = 409, body = ErrorResponse), (status = 422, body = ErrorResponse)))]
277277async fn add_cart_item(
278278 State(state): State<AppState>,
279279 Path(cart_id): Path<String>,
280280 payload: Result<Json<AddCartItemRequest>, JsonRejection>,
281281) -> Result<Json<CartResponse>, ApiError> {
282282 let Json(request) = payload?;
286286 Ok(Json(
287287 state
288288 .carts
290290 .await?
291291 .into(),
292292 ))
293293}
294294
295295#[utoipa::path(post, path = "/v1/carts/{id}/checkout", tag = "Checkout",
296296 params(("id" = String, Path, description = "Cart UUID")),
297297 responses((status = 201, body = OrderResponse), (status = 404, body = ErrorResponse),
298298 (status = 409, body = ErrorResponse), (status = 422, body = ErrorResponse)))]
299299async fn checkout(
300300 State(state): State<AppState>,
301301 Path(cart_id): Path<String>,
302302) -> Result<(StatusCode, Json<OrderResponse>), ApiError> {
305305 tracing::info!(order_id = order.id, cart_id = %order.cart_id, total_cents = order.total.cents(), "order placed");
306306 Ok((StatusCode::CREATED, Json(order.into())))
307307}
308308
309309#[derive(OpenApi)]
310310#[openapi(
…
337337)]
338338struct ApiDocument;
339339
340340async fn openapi() -> Json<utoipa::openapi::OpenApi> {
342342}
343343
344344pub fn router(state: AppState) -> Router {
345345 Router::new()
354354 .with_state(state)
355355}
356356
357357#[cfg(test)]
358358mod tests {
shop-rs-api/src/main.rs: 41/41 covered lines
runoldnew±codecov
1212const DEFAULT_ADDRESS: &str = "0.0.0.0:3000";
1313
1414#[tokio::main]
1515async fn main() {
1616 tracing_subscriber::fmt()
1717 .with_env_filter(EnvFilter::from_default_env())
1818 .init();
2020 error!(error = %message, "shop-rs-api failed");
2121 std::process::exit(1);
2222 }
2323}
2424
2525async fn run() -> Result<(), String> {
2727 let database_url =
2828 std::env::var("DATABASE_URL").map_err(|_| "DATABASE_URL is not set".to_owned())?;
3030 .await
3131 .map_err(|error| format!("database: {error:?}"))?;
3333 let state = AppState {
3636 products,
3939 )),
4040 };
4141 let listener = TcpListener::bind(&address)
4242 .await
4444 tracing::info!(%address, "shop-rs-api listening");
4747 .await
4848 .map_err(|error| format!("serve: {error}"))
4949}
5050
5151fn bind_failure(address: &str, error: &std::io::Error) -> String {
5252 error!(error = %error, %address, "bind failed");
5353 format!("bind {address}: {error}")
5454}
5555
5656#[rustfmt::skip]
5757async fn wait_for_shutdown() {
5858 #[cfg(unix)]
5959 if let Ok(mut terminate) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { terminate.recv().await; }
6060}
shop / shop-rs-api / unit
adapter.stdout tests.stderr tests.stdout adapter.stderr- Passed domain::model::tests::add_to_cart::merges_quantity_with_existing_line (0 ms)
- Passed application::tests::catalog_service::keeps_a_rejected_domain_error (0 ms)
- Passed application::tests::catalog_service::returns_not_found_for_unknown_sku (0 ms)
- Passed application::tests::catalog_service::surfaces_unavailable_storage (0 ms)
- Passed domain::model::tests::add_to_cart::rejects_checked_out_cart (0 ms)
- Passed application::tests::add_to_cart::rejects_quantity_beyond_stock (0 ms)
- Passed application::tests::add_to_cart::persists_merged_line (0 ms)
- Passed application::tests::checkout::does_not_place_order_for_empty_cart (0 ms)
- Passed application::tests::checkout::places_order_with_priced_total (0 ms)
- Passed application::tests::add_to_cart::rejects_unknown_cart (0 ms)
- Passed domain::model::tests::add_to_cart::rejects_merged_quantity_beyond_line_maximum (0 ms)
- Passed domain::model::tests::add_to_cart::rejects_quantity_beyond_stock (0 ms)
- Passed domain::model::tests::cart_id_parsing::normalizes_to_lowercase (0 ms)
- Passed domain::model::tests::cart_id_parsing::rejects_non_uuid (0 ms)
- Passed domain::model::tests::cart_id_rejects_a_dash_in_the_wrong_place (0 ms)
- Passed domain::model::tests::checkout::rejects_a_line_whose_product_is_missing (0 ms)
- Passed domain::model::tests::checkout::rejects_a_total_that_overflows (0 ms)
- Passed domain::model::tests::checkout::rejects_checked_out_cart (0 ms)
- Passed domain::model::tests::checkout::rejects_empty_cart (0 ms)
- Passed domain::model::tests::checkout::rejects_line_that_exceeds_current_stock (0 ms)
- Failed application::tests::catalog_service::reports_a_price_mismatch (0 ms)
- Passed domain::model::tests::checkout::totals_every_line_in_cents (0 ms)
- Passed domain::model::tests::money_rejects_a_negative_amount_and_an_overflowing_sum (0 ms)
- Passed domain::model::tests::quantity_bounds::accepts_every_value_in_range (0 ms)
- Passed domain::model::tests::every_domain_error_names_the_problem (0 ms)
- Passed domain::model::tests::quantity_bounds::rejects_above_line_maximum (0 ms)
- Passed domain::model::tests::quantity_bounds::rejects_zero (0 ms)
- Passed domain::model::tests::sku_accepts_the_longest_code_and_exposes_it (0 ms)
- Passed domain::model::tests::sku_parsing::accepts_uppercase_digits_and_dashes (0 ms)
- Passed domain::model::tests::sku_parsing::rejects_lowercase (0 ms)
- Passed domain::model::tests::sku_parsing::rejects_too_short (0 ms)
- Passed domain::port::tests::display_includes_the_domain_message_or_the_storage_reason (0 ms)
- Passed infrastructure::postgres::tests::cart_line_parts_reject_a_bad_sku_or_quantity (0 ms)
- Passed infrastructure::postgres::tests::cart_status_accepts_both_stored_values_and_rejects_anything_else (0 ms)
- Passed infrastructure::postgres::tests::order_outcomes_distinguish_a_closed_cart_and_a_missed_reservation (0 ms)
- Passed infrastructure::postgres::tests::product_parts_reject_values_the_schema_also_forbids (0 ms)
- Passed infrastructure::postgres::tests::quantity_column_rejects_a_value_that_does_not_fit_the_database (0 ms)
- Passed infrastructure::web::tests::every_domain_error_has_an_http_status (0 ms)
- Passed infrastructure::web::tests::catalog_reports_storage_failure (0 ms)
- Passed infrastructure::web::tests::cart_storage_failure_is_a_server_error (0 ms)
- Passed infrastructure::web::tests::catalog_lists_and_finds_products (0 ms)
- Passed infrastructure::web::tests::cart_opens_adds_and_checks_out (0 ms)
- Passed infrastructure::web::tests::checkout_reports_an_empty_cart_stock_conflict_and_overflow (0 ms)
- Passed infrastructure::web::tests::cart_rejects_bad_input_and_conflicts (0 ms)
- Passed infrastructure::web::tests::health_and_openapi_respond (0 ms)
- Passed infrastructure::postgres::tests::connect_reports_unavailable_when_the_server_refuses (0 ms)
shop / shop-ts-lib-cart / integration
adapter.stdout tests.stderr tests.stdout adapter.stderr- Passed published cart entry prices two mugs through the package export (7 ms)
shop-ts-lib-cart/src/cart.ts: 23/33 covered lines
runoldnew±codecov
66 readonly unitPriceCents: Cents;
77}
88
99export class InvalidAmountError extends Error {
1010 constructor(readonly amount: number) {
1111 super(`amount must be a non-negative safe integer of cents, got ${amount}`);
1212 this.name = "InvalidAmountError";
1313 }
1414}
1515
1616export class InvalidQuantityError extends Error {
1717 constructor(readonly sku: string, readonly quantity: number) {
1818 super(`quantity for ${sku} must be an integer between 1 and 99, got ${quantity}`);
1919 this.name = "InvalidQuantityError";
2020 }
2121}
2222
2323const MINIMUM_QUANTITY = 1;
2424const MAXIMUM_QUANTITY = 99;
2525const CENTS_PER_UNIT = 100;
2626
2727export function cents(amount: number): Cents {
2828 if (!Number.isSafeInteger(amount) || amount < 0) {
3030 }
3131 return amount as Cents;
3232}
3333
3535 if (!Number.isInteger(line.quantity) || line.quantity < MINIMUM_QUANTITY || line.quantity > MAXIMUM_QUANTITY) {
3737 }
3939}
4040
4343}
4444
4545export function formatCents(amount: Cents, currency: string, locale: string): string {
4646 return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount / CENTS_PER_UNIT);
4747}
shop / shop-ts-lib-cart / unit
adapter.stdout tests.stderr tests.stdout adapter.stderr- Passed cents accepts zero (0 ms)
- Passed cents rejects negative amounts (0 ms)
- Passed cents rejects fractional amounts (0 ms)
- Passed lineTotal multiplies unit price by quantity (0 ms)
- Passed lineTotal rejects a zero quantity (0 ms)
- Passed lineTotal rejects a quantity above the cart limit (0 ms)
- Passed cartTotal is zero for an empty cart (0 ms)
- Passed cartTotal sums every line (0 ms)
- Passed formatCents formats euros for an Italian locale (27 ms)
- Passed formatCents formats dollars for a US locale (0 ms)
shop-ts-lib-cart/src/cart.ts: 33/33 covered lines
runoldnew±codecov
66 readonly unitPriceCents: Cents;
77}
88
99export class InvalidAmountError extends Error {
1010 constructor(readonly amount: number) {
1111 super(`amount must be a non-negative safe integer of cents, got ${amount}`);
1212 this.name = "InvalidAmountError";
1313 }
1414}
1515
1616export class InvalidQuantityError extends Error {
1717 constructor(readonly sku: string, readonly quantity: number) {
1818 super(`quantity for ${sku} must be an integer between 1 and 99, got ${quantity}`);
1919 this.name = "InvalidQuantityError";
2020 }
2121}
2222
2323const MINIMUM_QUANTITY = 1;
2424const MAXIMUM_QUANTITY = 99;
2525const CENTS_PER_UNIT = 100;
2626
2727export function cents(amount: number): Cents {
2828 if (!Number.isSafeInteger(amount) || amount < 0) {
3030 }
3131 return amount as Cents;
3232}
3333
3535 if (!Number.isInteger(line.quantity) || line.quantity < MINIMUM_QUANTITY || line.quantity > MAXIMUM_QUANTITY) {
3737 }
3939}
4040
4343}
4444
4545export function formatCents(amount: Cents, currency: string, locale: string): string {
4646 return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount / CENTS_PER_UNIT);
4747}
shop / shop-ts-web-react / functional
probe-bind.stderr adapter.stdout tests.stdout adapter.stderr vite.stderr api.stderr probe-missing-database.stderr build.log- Passed shows the seeded catalog (316 ms)
- Passed rejects a zero quantity (203 ms)
- Passed rejects checkout of an empty cart (209 ms)
- Passed checks out two mugs and a second empty cart (256 ms)
- Passed rejects a lamp quantity above the stock on hand (139 ms)
- Passed shows an unknown product when the catalog row disappears (277 ms)
shop-ts-lib-cart/src/cart.ts: 9/9 covered lines
runoldnew±codecov
2020 }
2121}
2222
2323const MINIMUM_QUANTITY = 1;
2424const MAXIMUM_QUANTITY = 99;
2525const CENTS_PER_UNIT = 100;
2626
2727export function cents(amount: number): Cents {
2828 if (!Number.isSafeInteger(amount) || amount < 0) {
2929 throw new InvalidAmountError(amount);
3030 }
3131 return amount as Cents;
3232}
3333
3535 if (!Number.isInteger(line.quantity) || line.quantity < MINIMUM_QUANTITY || line.quantity > MAXIMUM_QUANTITY) {
3636 throw new InvalidQuantityError(line.sku, line.quantity);
3737 }
3939}
4040
4343}
4444
4545export function formatCents(amount: Cents, currency: string, locale: string): string {
4646 return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount / CENTS_PER_UNIT);
4747}
shop-ts-web-react/src/Shop.tsx: 47/50 covered lines
runoldnew±codecov
33
44import { addCartItem, checkoutCart, listProducts, openCart, ShopRequestError, type Cart, type Order, type Product } from "./shopApi";
55
66const CURRENCY = "USD";
77const LOCALE = "en-US";
88
99interface Props {
1010 readonly apiBase: string;
1111}
1212
1515 return { sku: line.sku, quantity: line.quantity, unitPriceCents: cents(product?.priceCents ?? 0) };
1616}
1717
1818function messageOf(error: unknown): string {
2020 return "something went wrong";
2121}
2222
2727 const [quantities, setQuantities] = useState<Record<string, string>>({});
2828 const [error, setError] = useState<string | null>(null);
2929 const [busy, setBusy] = useState(false);
3030
3131 useEffect(() => {
3232 let active = true;
3434 .then((loaded) => {
3535 if (active) setProducts(loaded);
3636 })
3737 .catch((failure: unknown) => {
3939 });
4040 return () => {
4141 active = false;
4242 };
4343 }, [apiBase]);
4444
4646 const parsed = Number(quantities[product.sku] ?? "1");
4747 setBusy(true);
4848 setError(null);
4949 setOrder(null);
5050 try {
5353 setCart(updated);
5555 setProducts(refreshed);
5656 } catch (failure: unknown) {
5858 } finally {
5959 setBusy(false);
6060 }
6161 }
6262
6363 async function checkout() {
6464 setBusy(true);
6565 setError(null);
6666 try {
6969 setOrder(placed);
7070 setCart(null);
7272 } catch (failure: unknown) {
7474 } finally {
7575 setBusy(false);
7676 }
7777 }
7878
8181
8282 return (
8585 {products === null && error === null ? <p>Loading the catalog.</p> : null}
8686 {error ? <p role="alert">{error}</p> : null}
8787 <ul>
8888 {(products ?? []).map((product) => (
8989 <li key={product.sku}>
9090 <span>{product.name}</span>
9292 <span>{product.stock} in stock</span>
…
9595 <input
9696 aria-label={`Quantity for ${product.name}`}
9999 />
100100 </label>
102102 Add {product.name}
103103 </button>
104104 </li>
…
109109 {lines.length === 0 ? <p>The cart is empty.</p> : null}
110110 <ul>
111111 {lines.map((line) => {
113113 return (
114114 <li key={line.sku}>
115115 <span>{product?.name ?? line.sku}</span>
116116 <span>{line.quantity}</span>
…
120120 })}
121121 </ul>
124124 Checkout
125125 </button>
126126 </section>
shop-ts-web-react/src/main.tsx: 1/1 covered lines
shop-ts-web-react/src/mount.tsx: 2/2 covered lines
shop-ts-web-react/src/shopApi.ts: 22/24 covered lines
runoldnew±codecov
2828 readonly code: string,
2929 message: string,
3030 ) {
3131 super(message);
3232 this.name = "ShopRequestError";
3333 }
3434}
3535
…
5353}
5454
5656 return { sku: body.sku, name: body.name, priceCents: body.price_cents, stock: body.stock };
5757}
5858
6060 return {
6161 id: body.id,
6262 status: body.status,
6363 lines: body.lines.map((line) => ({ sku: line.sku, quantity: line.quantity })),
6464 };
6565}
6666
6868 let code = "request_failed";
6969 let message = response.statusText;
7070 try {
7171 const body = (await response.json()) as { error?: { code?: string; message?: string } };
7272 if (body.error?.code) code = body.error.code;
7373 if (body.error?.message) message = body.error.message;
7474 } catch {
7575 message = "the shop could not read the response";
7676 }
7878}
7979
8080async function requestJson<T>(apiBase: string, path: string, init?: RequestInit): Promise<T> {
8181 let response: Response;
8282 try {
8383 response = await fetch(`${apiBase}${path}`, init);
8484 } catch {
8686 }
8888 return (await response.json()) as T;
8989}
9090
9494}
9595
9898}
9999
100100export async function addCartItem(apiBase: string, cartId: string, sku: string, quantity: number): Promise<Cart> {
103103 method: "POST",
104104 headers: { "content-type": "application/json" },
…
108108}
109109
111111 const body = await requestJson<OrderBody>(apiBase, `/v1/carts/${cartId}/checkout`, { method: "POST" });
112112 return { id: body.id, cartId: body.cart_id, totalCents: body.total_cents };
113113}
shop / shop-ts-web-react / unit
adapter.stdout tests.stderr tests.stdout adapter.stderr- Passed mountShop rejects a missing root (0 ms)
- Passed mountShop renders the shop into the root element (39 ms)
- Passed Shop shows the catalog and an empty cart (27 ms)
- Passed Shop shows a loading line until the catalog arrives (4 ms)
- Passed Shop ignores a catalog response that arrives after unmount (1 ms)
- Passed Shop shows a storage failure from the catalog (16 ms)
- Passed Shop shows a generic message when the catalog body is not a list (4 ms)
- Passed Shop shows a network failure (3 ms)
- Passed Shop adds two mugs and shows the line total (39 ms)
- Passed Shop disables actions while an add is in flight (16 ms)
- Passed Shop shows the sku when a cart line is no longer in the catalog (14 ms)
- Passed Shop places an order and rejects the next empty checkout (34 ms)
- Passed Shop shows an empty-cart error and a second checkout error (19 ms)
- Passed Shop shows a zero quantity error (25 ms)
- Passed Shop shows an out of stock error (24 ms)
- Passed Shop shows an unreadable add failure (13 ms)
- Passed shop API maps catalog rows into products (2 ms)
- Passed shop API opens a cart (0 ms)
- Passed shop API adds a line and returns the cart (0 ms)
- Passed shop API maps a placed order (0 ms)
- Passed shop API reads an API error (0 ms)
- Passed shop API keeps the status text when the error body has no code (0 ms)
- Passed shop API reports an unreadable error body (0 ms)
- Passed shop API reports a network failure (0 ms)
shop-ts-web-react/src/Shop.tsx: 114/114 covered lines
runoldnew±codecov
11import { useEffect, useState } from "react";
22import { cartTotal, cents, formatCents, lineTotal, type CartLine as PricedLine } from "shop-ts-lib-cart";
33
44import { addCartItem, checkoutCart, listProducts, openCart, ShopRequestError, type Cart, type Order, type Product } from "./shopApi";
55
66const CURRENCY = "USD";
77const LOCALE = "en-US";
88
99interface Props {
1010 readonly apiBase: string;
1111}
1212
1515 return { sku: line.sku, quantity: line.quantity, unitPriceCents: cents(product?.priceCents ?? 0) };
1616}
1717
1818function messageOf(error: unknown): string {
2020 return "something went wrong";
2121}
2222
2727 const [quantities, setQuantities] = useState<Record<string, string>>({});
2828 const [error, setError] = useState<string | null>(null);
2929 const [busy, setBusy] = useState(false);
3030
3131 useEffect(() => {
3232 let active = true;
3434 .then((loaded) => {
3535 if (active) setProducts(loaded);
3636 })
3737 .catch((failure: unknown) => {
3939 });
4040 return () => {
4141 active = false;
4242 };
4343 }, [apiBase]);
4444
4646 const parsed = Number(quantities[product.sku] ?? "1");
4747 setBusy(true);
4848 setError(null);
4949 setOrder(null);
5050 try {
5353 setCart(updated);
5555 setProducts(refreshed);
5656 } catch (failure: unknown) {
5858 } finally {
5959 setBusy(false);
6060 }
6161 }
6262
6363 async function checkout() {
6464 setBusy(true);
6565 setError(null);
6666 try {
6969 setOrder(placed);
7070 setCart(null);
7272 } catch (failure: unknown) {
7474 } finally {
7575 setBusy(false);
7676 }
7777 }
7878
8181
8282 return (
8585 {products === null && error === null ? <p>Loading the catalog.</p> : null}
8686 {error ? <p role="alert">{error}</p> : null}
8787 <ul>
8888 {(products ?? []).map((product) => (
8989 <li key={product.sku}>
9090 <span>{product.name}</span>
9292 <span>{product.stock} in stock</span>
9393 <label>
9595 <input
9696 aria-label={`Quantity for ${product.name}`}
9999 />
100100 </label>
102102 Add {product.name}
103103 </button>
104104 </li>
105105 ))}
106106 </ul>
107107 <section aria-label="Cart">
109109 {lines.length === 0 ? <p>The cart is empty.</p> : null}
110110 <ul>
111111 {lines.map((line) => {
113113 return (
114114 <li key={line.sku}>
115115 <span>{product?.name ?? line.sku}</span>
116116 <span>{line.quantity}</span>
118118 </li>
119119 );
120120 })}
121121 </ul>
124124 Checkout
125125 </button>
126126 </section>
127127 {order ? (
128128 <p role="status">
130130 </p>
131131 ) : null}
133133 );
134134}
135135
136136
shop-ts-web-react/src/mount.tsx: 5/5 covered lines
shop-ts-web-react/src/shopApi.ts: 62/62 covered lines
runoldnew±codecov
2222 readonly totalCents: number;
2323}
2424
2525export class ShopRequestError extends Error {
2626 constructor(
2727 readonly status: number,
2828 readonly code: string,
2929 message: string,
3030 ) {
3131 super(message);
3232 this.name = "ShopRequestError";
3333 }
3434}
3535
3636interface ProductBody {
3737 readonly sku: string;
…
5252 readonly total_cents: number;
5353}
5454
5656 return { sku: body.sku, name: body.name, priceCents: body.price_cents, stock: body.stock };
5757}
5858
6060 return {
6161 id: body.id,
6262 status: body.status,
6363 lines: body.lines.map((line) => ({ sku: line.sku, quantity: line.quantity })),
6464 };
6565}
6666
6868 let code = "request_failed";
6969 let message = response.statusText;
7070 try {
7171 const body = (await response.json()) as { error?: { code?: string; message?: string } };
7272 if (body.error?.code) code = body.error.code;
7373 if (body.error?.message) message = body.error.message;
7474 } catch {
7575 message = "the shop could not read the response";
7676 }
7878}
7979
8080async function requestJson<T>(apiBase: string, path: string, init?: RequestInit): Promise<T> {
8181 let response: Response;
8282 try {
8383 response = await fetch(`${apiBase}${path}`, init);
8484 } catch {
8686 }
8888 return (await response.json()) as T;
8989}
9090
9494}
9595
9898}
9999
100100export async function addCartItem(apiBase: string, cartId: string, sku: string, quantity: number): Promise<Cart> {
103103 method: "POST",
104104 headers: { "content-type": "application/json" },
105105 body: JSON.stringify({ sku, quantity }),
106106 }),
107107 );
108108}
109109
111111 const body = await requestJson<OrderBody>(apiBase, `/v1/carts/${cartId}/checkout`, { method: "POST" });
112112 return { id: body.id, cartId: body.cart_id, totalCents: body.total_cents };
113113}
shop / shop-ts-web-react / visual
probe-bind.stderr adapter.stdout tests.stdout adapter.stderr vite.stderr api.stderr probe-missing-database.stderr build.log- Passed catalog (567 ms)
- Passed empty checkout (389 ms)
- Passed cart lines (365 ms)
- Passed order placed (359 ms)
shop-ts-lib-cart/src/cart.ts: 9/9 covered lines
runoldnew±codecov
2020 }
2121}
2222
2323const MINIMUM_QUANTITY = 1;
2424const MAXIMUM_QUANTITY = 99;
2525const CENTS_PER_UNIT = 100;
2626
2727export function cents(amount: number): Cents {
2828 if (!Number.isSafeInteger(amount) || amount < 0) {
2929 throw new InvalidAmountError(amount);
3030 }
3131 return amount as Cents;
3232}
3333
3535 if (!Number.isInteger(line.quantity) || line.quantity < MINIMUM_QUANTITY || line.quantity > MAXIMUM_QUANTITY) {
3636 throw new InvalidQuantityError(line.sku, line.quantity);
3737 }
3939}
4040
4343}
4444
4545export function formatCents(amount: Cents, currency: string, locale: string): string {
4646 return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount / CENTS_PER_UNIT);
4747}
shop-ts-web-react/src/Shop.tsx: 46/50 covered lines
runoldnew±codecov
33
44import { addCartItem, checkoutCart, listProducts, openCart, ShopRequestError, type Cart, type Order, type Product } from "./shopApi";
55
66const CURRENCY = "USD";
77const LOCALE = "en-US";
88
99interface Props {
1010 readonly apiBase: string;
1111}
1212
1515 return { sku: line.sku, quantity: line.quantity, unitPriceCents: cents(product?.priceCents ?? 0) };
1616}
1717
1818function messageOf(error: unknown): string {
2020 return "something went wrong";
2121}
2222
2727 const [quantities, setQuantities] = useState<Record<string, string>>({});
2828 const [error, setError] = useState<string | null>(null);
2929 const [busy, setBusy] = useState(false);
3030
3131 useEffect(() => {
3232 let active = true;
3434 .then((loaded) => {
3535 if (active) setProducts(loaded);
3636 })
3737 .catch((failure: unknown) => {
3939 });
4040 return () => {
4141 active = false;
4242 };
4343 }, [apiBase]);
4444
4646 const parsed = Number(quantities[product.sku] ?? "1");
4747 setBusy(true);
4848 setError(null);
4949 setOrder(null);
5050 try {
5353 setCart(updated);
5555 setProducts(refreshed);
5656 } catch (failure: unknown) {
5858 } finally {
5959 setBusy(false);
6060 }
6161 }
6262
6363 async function checkout() {
6464 setBusy(true);
6565 setError(null);
6666 try {
6969 setOrder(placed);
7070 setCart(null);
7272 } catch (failure: unknown) {
7474 } finally {
7575 setBusy(false);
7676 }
7777 }
7878
8181
8282 return (
8585 {products === null && error === null ? <p>Loading the catalog.</p> : null}
8686 {error ? <p role="alert">{error}</p> : null}
8787 <ul>
8888 {(products ?? []).map((product) => (
8989 <li key={product.sku}>
9090 <span>{product.name}</span>
9292 <span>{product.stock} in stock</span>
…
9595 <input
9696 aria-label={`Quantity for ${product.name}`}
9999 />
100100 </label>
102102 Add {product.name}
103103 </button>
104104 </li>
…
109109 {lines.length === 0 ? <p>The cart is empty.</p> : null}
110110 <ul>
111111 {lines.map((line) => {
113113 return (
114114 <li key={line.sku}>
115115 <span>{product?.name ?? line.sku}</span>
116116 <span>{line.quantity}</span>
…
120120 })}
121121 </ul>
124124 Checkout
125125 </button>
126126 </section>
shop-ts-web-react/src/main.tsx: 1/1 covered lines
shop-ts-web-react/src/mount.tsx: 2/2 covered lines
shop-ts-web-react/src/shopApi.ts: 22/24 covered lines
runoldnew±codecov
2828 readonly code: string,
2929 message: string,
3030 ) {
3131 super(message);
3232 this.name = "ShopRequestError";
3333 }
3434}
3535
…
5353}
5454
5656 return { sku: body.sku, name: body.name, priceCents: body.price_cents, stock: body.stock };
5757}
5858
6060 return {
6161 id: body.id,
6262 status: body.status,
6363 lines: body.lines.map((line) => ({ sku: line.sku, quantity: line.quantity })),
6464 };
6565}
6666
6868 let code = "request_failed";
6969 let message = response.statusText;
7070 try {
7171 const body = (await response.json()) as { error?: { code?: string; message?: string } };
7272 if (body.error?.code) code = body.error.code;
7373 if (body.error?.message) message = body.error.message;
7474 } catch {
7575 message = "the shop could not read the response";
7676 }
7878}
7979
8080async function requestJson<T>(apiBase: string, path: string, init?: RequestInit): Promise<T> {
8181 let response: Response;
8282 try {
8383 response = await fetch(`${apiBase}${path}`, init);
8484 } catch {
8686 }
8888 return (await response.json()) as T;
8989}
9090
9494}
9595
9898}
9999
100100export async function addCartItem(apiBase: string, cartId: string, sku: string, quantity: number): Promise<Cart> {
103103 method: "POST",
104104 headers: { "content-type": "application/json" },
…
108108}
109109
111111 const body = await requestJson<OrderBody>(apiBase, `/v1/carts/${cartId}/checkout`, { method: "POST" });
112112 return { id: body.id, cartId: body.cart_id, totalCents: body.total_cents };
113113}