| 6 |
6 |
|
use makenotwork::db::UserId;
|
| 7 |
7 |
|
use serde_json::Value;
|
| 8 |
8 |
|
use std::collections::HashMap;
|
| 9 |
|
- |
use stripe::{Event, EventObject, EventType, NotificationEventData};
|
| 10 |
9 |
|
|
| 11 |
|
- |
/// Build an Event in-memory, serialize to JSON, sign, and POST to /stripe/webhook.
|
| 12 |
|
- |
async fn post_event(h: &mut TestHarness, event: &Event) -> crate::harness::client::TestResponse {
|
| 13 |
|
- |
let payload = serde_json::to_string(event).expect("Event serialization");
|
|
10 |
+ |
/// Build a JSON event with the given type and object, sign it, and POST to /stripe/webhook.
|
|
11 |
+ |
async fn post_event_json(
|
|
12 |
+ |
h: &mut TestHarness,
|
|
13 |
+ |
event_type: &str,
|
|
14 |
+ |
object: serde_json::Value,
|
|
15 |
+ |
) -> crate::harness::client::TestResponse {
|
|
16 |
+ |
post_event_json_with_id(h, "evt_test_000", event_type, object).await
|
|
17 |
+ |
}
|
|
18 |
+ |
|
|
19 |
+ |
async fn post_event_json_with_id(
|
|
20 |
+ |
h: &mut TestHarness,
|
|
21 |
+ |
event_id: &str,
|
|
22 |
+ |
event_type: &str,
|
|
23 |
+ |
object: serde_json::Value,
|
|
24 |
+ |
) -> crate::harness::client::TestResponse {
|
|
25 |
+ |
let payload = serde_json::json!({
|
|
26 |
+ |
"id": event_id,
|
|
27 |
+ |
"type": event_type,
|
|
28 |
+ |
"data": {"object": object},
|
|
29 |
+ |
})
|
|
30 |
+ |
.to_string();
|
| 14 |
31 |
|
let signature = sign_webhook_payload(&payload, TEST_WEBHOOK_SECRET);
|
| 15 |
32 |
|
|
| 16 |
33 |
|
h.client
|
| 26 |
43 |
|
.await
|
| 27 |
44 |
|
}
|
| 28 |
45 |
|
|
| 29 |
|
- |
/// Construct a minimal Event with the given type and object.
|
| 30 |
|
- |
fn make_event(event_type: EventType, object: EventObject) -> Event {
|
| 31 |
|
- |
Event {
|
| 32 |
|
- |
id: "evt_test_000".parse().unwrap(),
|
| 33 |
|
- |
type_: event_type,
|
| 34 |
|
- |
data: NotificationEventData {
|
| 35 |
|
- |
object,
|
| 36 |
|
- |
..Default::default()
|
| 37 |
|
- |
},
|
| 38 |
|
- |
..Default::default()
|
| 39 |
|
- |
}
|
| 40 |
|
- |
}
|
| 41 |
|
- |
|
| 42 |
46 |
|
// ---------------------------------------------------------------------------
|
| 43 |
47 |
|
// Tests
|
| 44 |
48 |
|
// ---------------------------------------------------------------------------
|
| 85 |
89 |
|
.unwrap();
|
| 86 |
90 |
|
|
| 87 |
91 |
|
// Build account object with valid id prefix
|
| 88 |
|
- |
let account: stripe::Account = serde_json::from_value(serde_json::json!({
|
|
92 |
+ |
let account = serde_json::json!({
|
| 89 |
93 |
|
"id": acct_id,
|
|
94 |
+ |
"object": "account",
|
| 90 |
95 |
|
"charges_enabled": true,
|
| 91 |
96 |
|
"payouts_enabled": true,
|
| 92 |
97 |
|
"details_submitted": true,
|
| 93 |
|
- |
}))
|
| 94 |
|
- |
.unwrap();
|
|
98 |
+ |
});
|
| 95 |
99 |
|
|
| 96 |
|
- |
let event = make_event(EventType::AccountUpdated, EventObject::Account(account));
|
| 97 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
100 |
+ |
let resp = post_event_json(&mut h, "account.updated", account).await;
|
| 98 |
101 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 99 |
102 |
|
|
| 100 |
103 |
|
// Verify DB was updated
|
| 161 |
164 |
|
meta.insert("buyer_id".to_string(), buyer_id.to_string());
|
| 162 |
165 |
|
meta.insert("seller_id".to_string(), seller_id.to_string());
|
| 163 |
166 |
|
meta.insert("item_id".to_string(), item_id.clone());
|
| 164 |
|
- |
let session = stripe::CheckoutSession {
|
| 165 |
|
- |
id: session_id.parse().unwrap(),
|
| 166 |
|
- |
metadata: Some(meta),
|
| 167 |
|
- |
payment_intent: Some(serde_json::from_value(serde_json::json!("pi_test_purchase_123")).unwrap()),
|
| 168 |
|
- |
..Default::default()
|
| 169 |
|
- |
};
|
| 170 |
|
- |
|
| 171 |
|
- |
let event = make_event(
|
| 172 |
|
- |
EventType::CheckoutSessionCompleted,
|
| 173 |
|
- |
EventObject::CheckoutSession(session),
|
| 174 |
|
- |
);
|
| 175 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
167 |
+ |
let session = serde_json::json!({
|
|
168 |
+ |
"id": session_id,
|
|
169 |
+ |
"object": "checkout_session",
|
|
170 |
+ |
"mode": "payment",
|
|
171 |
+ |
"metadata": meta,
|
|
172 |
+ |
"payment_intent": "pi_test_purchase_123",
|
|
173 |
+ |
});
|
|
174 |
+ |
|
|
175 |
+ |
let resp = post_event_json(&mut h, "checkout.session.completed", session).await;
|
| 176 |
176 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 177 |
177 |
|
|
| 178 |
178 |
|
// Verify transaction was completed
|
| 245 |
245 |
|
.unwrap();
|
| 246 |
246 |
|
|
| 247 |
247 |
|
// Build charge with valid id and payment_intent
|
| 248 |
|
- |
let charge = stripe::Charge {
|
| 249 |
|
- |
id: "ch_test_refund".parse().unwrap(),
|
| 250 |
|
- |
payment_intent: Some(serde_json::from_value(serde_json::json!("pi_test_refund_123")).unwrap()),
|
| 251 |
|
- |
..Default::default()
|
| 252 |
|
- |
};
|
| 253 |
|
- |
|
| 254 |
|
- |
let event = make_event(EventType::ChargeRefunded, EventObject::Charge(charge));
|
| 255 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
248 |
+ |
let charge = serde_json::json!({
|
|
249 |
+ |
"id": "ch_test_refund",
|
|
250 |
+ |
"object": "charge",
|
|
251 |
+ |
"amount": 500,
|
|
252 |
+ |
"amount_refunded": 500,
|
|
253 |
+ |
"payment_intent": "pi_test_refund_123",
|
|
254 |
+ |
});
|
|
255 |
+ |
|
|
256 |
+ |
let resp = post_event_json(&mut h, "charge.refunded", charge).await;
|
| 256 |
257 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 257 |
258 |
|
|
| 258 |
259 |
|
// Verify transaction was refunded
|
| 321 |
322 |
|
.await
|
| 322 |
323 |
|
.unwrap();
|
| 323 |
324 |
|
|
| 324 |
|
- |
// Build subscription object — must use from_value because Subscription has
|
| 325 |
|
- |
// required non-Option fields (customer, items, etc.) whose Default values
|
| 326 |
|
- |
// don't survive a serde round-trip (empty IDs fail prefix validation).
|
| 327 |
|
- |
let sub: stripe::Subscription = serde_json::from_value(serde_json::json!({
|
|
325 |
+ |
let sub = serde_json::json!({
|
| 328 |
326 |
|
"id": stripe_sub_id,
|
|
327 |
+ |
"object": "subscription",
|
| 329 |
328 |
|
"status": "canceled",
|
| 330 |
|
- |
"customer": "cus_test_sub_123",
|
| 331 |
|
- |
"currency": "usd",
|
| 332 |
329 |
|
"cancel_at_period_end": false,
|
| 333 |
330 |
|
"items": {
|
| 334 |
331 |
|
"object": "list",
|
| 335 |
|
- |
"data": [],
|
| 336 |
|
- |
"has_more": false,
|
| 337 |
|
- |
"url": "/v1/subscription_items"
|
|
332 |
+ |
"data": [{
|
|
333 |
+ |
"id": "si_test_del",
|
|
334 |
+ |
"object": "subscription_item",
|
|
335 |
+ |
"subscription": stripe_sub_id,
|
|
336 |
+ |
"current_period_start": 1700000000,
|
|
337 |
+ |
"current_period_end": 1702592000,
|
|
338 |
+ |
"metadata": {},
|
|
339 |
+ |
}],
|
| 338 |
340 |
|
},
|
| 339 |
|
- |
"automatic_tax": { "enabled": false },
|
| 340 |
|
- |
"billing_cycle_anchor": 1700000000,
|
| 341 |
|
- |
"current_period_start": 1700000000,
|
| 342 |
|
- |
"current_period_end": 1702592000,
|
| 343 |
|
- |
"created": 1700000000,
|
| 344 |
|
- |
"start_date": 1700000000,
|
| 345 |
|
- |
"livemode": false,
|
| 346 |
|
- |
"metadata": {},
|
| 347 |
|
- |
}))
|
| 348 |
|
- |
.expect("Subscription deserialization");
|
|
341 |
+ |
});
|
| 349 |
342 |
|
|
| 350 |
|
- |
let event = make_event(
|
| 351 |
|
- |
EventType::CustomerSubscriptionDeleted,
|
| 352 |
|
- |
EventObject::Subscription(sub),
|
| 353 |
|
- |
);
|
| 354 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
343 |
+ |
let resp = post_event_json(&mut h, "customer.subscription.deleted", sub).await;
|
| 355 |
344 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 356 |
345 |
|
|
| 357 |
346 |
|
// Verify subscription was canceled
|
| 453 |
442 |
|
meta.insert("subscriber_id".to_string(), fix.subscriber_id.to_string());
|
| 454 |
443 |
|
meta.insert("project_id".to_string(), fix.project_id.clone());
|
| 455 |
444 |
|
meta.insert("tier_id".to_string(), fix.tier_id.to_string());
|
| 456 |
|
- |
let session = stripe::CheckoutSession {
|
| 457 |
|
- |
id: "cs_test_sub_checkout_001".parse().unwrap(),
|
| 458 |
|
- |
metadata: Some(meta),
|
| 459 |
|
- |
subscription: Some(serde_json::from_value(serde_json::json!(stripe_sub_id)).unwrap()),
|
| 460 |
|
- |
customer: Some(serde_json::from_value(serde_json::json!(stripe_customer_id)).unwrap()),
|
| 461 |
|
- |
..Default::default()
|
| 462 |
|
- |
};
|
|
445 |
+ |
let session = serde_json::json!({
|
|
446 |
+ |
"id": "cs_test_sub_checkout_001",
|
|
447 |
+ |
"object": "checkout_session",
|
|
448 |
+ |
"mode": "subscription",
|
|
449 |
+ |
"metadata": meta,
|
|
450 |
+ |
"subscription": stripe_sub_id,
|
|
451 |
+ |
"customer": stripe_customer_id,
|
|
452 |
+ |
});
|
| 463 |
453 |
|
|
| 464 |
|
- |
let event = make_event(
|
| 465 |
|
- |
EventType::CheckoutSessionCompleted,
|
| 466 |
|
- |
EventObject::CheckoutSession(session),
|
| 467 |
|
- |
);
|
| 468 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
454 |
+ |
let resp = post_event_json(&mut h, "checkout.session.completed", session).await;
|
| 469 |
455 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 470 |
456 |
|
|
| 471 |
457 |
|
// Verify subscription row was created
|
| 496 |
482 |
|
meta.insert("subscriber_id".to_string(), fix.subscriber_id.to_string());
|
| 497 |
483 |
|
meta.insert("project_id".to_string(), fix.project_id.clone());
|
| 498 |
484 |
|
meta.insert("tier_id".to_string(), fix.tier_id.to_string());
|
| 499 |
|
- |
stripe::CheckoutSession {
|
| 500 |
|
- |
id: "cs_test_sub_idem".parse().unwrap(),
|
| 501 |
|
- |
metadata: Some(meta),
|
| 502 |
|
- |
subscription: Some(serde_json::from_value(serde_json::json!(stripe_sub_id)).unwrap()),
|
| 503 |
|
- |
customer: Some(serde_json::from_value(serde_json::json!(stripe_customer_id)).unwrap()),
|
| 504 |
|
- |
..Default::default()
|
| 505 |
|
- |
}
|
|
485 |
+ |
serde_json::json!({
|
|
486 |
+ |
"id": "cs_test_sub_idem",
|
|
487 |
+ |
"object": "checkout_session",
|
|
488 |
+ |
"mode": "subscription",
|
|
489 |
+ |
"metadata": meta,
|
|
490 |
+ |
"subscription": stripe_sub_id,
|
|
491 |
+ |
"customer": stripe_customer_id,
|
|
492 |
+ |
})
|
| 506 |
493 |
|
};
|
| 507 |
494 |
|
|
| 508 |
495 |
|
// First event
|
| 509 |
|
- |
let event1 = make_event(
|
| 510 |
|
- |
EventType::CheckoutSessionCompleted,
|
| 511 |
|
- |
EventObject::CheckoutSession(build_session()),
|
| 512 |
|
- |
);
|
| 513 |
|
- |
let resp = post_event(&mut h, &event1).await;
|
|
496 |
+ |
let resp = post_event_json(&mut h, "checkout.session.completed", build_session()).await;
|
| 514 |
497 |
|
assert_eq!(resp.status.as_u16(), 200, "First webhook failed: {}", resp.text);
|
| 515 |
498 |
|
|
| 516 |
499 |
|
// Second event (duplicate) — use a different event ID
|
| 517 |
|
- |
let mut event2 = make_event(
|
| 518 |
|
- |
EventType::CheckoutSessionCompleted,
|
| 519 |
|
- |
EventObject::CheckoutSession(build_session()),
|
| 520 |
|
- |
);
|
| 521 |
|
- |
event2.id = "evt_test_001".parse().unwrap();
|
| 522 |
|
- |
let resp = post_event(&mut h, &event2).await;
|
|
500 |
+ |
let resp = post_event_json_with_id(&mut h, "evt_test_001", "checkout.session.completed", build_session()).await;
|
| 523 |
501 |
|
assert_eq!(resp.status.as_u16(), 200, "Duplicate webhook should succeed: {}", resp.text);
|
| 524 |
502 |
|
|
| 525 |
503 |
|
// Verify still only one subscription row
|
| 541 |
519 |
|
let stripe_sub_id = "sub_test_updated_001";
|
| 542 |
520 |
|
insert_active_subscription(&h, &fix, stripe_sub_id).await;
|
| 543 |
521 |
|
|
| 544 |
|
- |
// Build subscription with past_due status and new period
|
| 545 |
|
- |
let sub: stripe::Subscription = serde_json::from_value(serde_json::json!({
|
|
522 |
+ |
let sub = serde_json::json!({
|
| 546 |
523 |
|
"id": stripe_sub_id,
|
|
524 |
+ |
"object": "subscription",
|
| 547 |
525 |
|
"status": "past_due",
|
| 548 |
|
- |
"customer": "cus_test_fixture",
|
| 549 |
|
- |
"currency": "usd",
|
| 550 |
526 |
|
"cancel_at_period_end": false,
|
| 551 |
527 |
|
"items": {
|
| 552 |
528 |
|
"object": "list",
|
| 553 |
|
- |
"data": [],
|
| 554 |
|
- |
"has_more": false,
|
| 555 |
|
- |
"url": "/v1/subscription_items"
|
|
529 |
+ |
"data": [{
|
|
530 |
+ |
"id": "si_test_upd",
|
|
531 |
+ |
"object": "subscription_item",
|
|
532 |
+ |
"subscription": stripe_sub_id,
|
|
533 |
+ |
"current_period_start": 1702592000,
|
|
534 |
+ |
"current_period_end": 1705184000,
|
|
535 |
+ |
"metadata": {},
|
|
536 |
+ |
}],
|
| 556 |
537 |
|
},
|
| 557 |
|
- |
"automatic_tax": { "enabled": false },
|
| 558 |
|
- |
"billing_cycle_anchor": 1700000000,
|
| 559 |
|
- |
"current_period_start": 1702592000,
|
| 560 |
|
- |
"current_period_end": 1705184000,
|
| 561 |
|
- |
"created": 1700000000,
|
| 562 |
|
- |
"start_date": 1700000000,
|
| 563 |
|
- |
"livemode": false,
|
| 564 |
|
- |
"metadata": {},
|
| 565 |
|
- |
}))
|
| 566 |
|
- |
.expect("Subscription deserialization");
|
|
538 |
+ |
});
|
| 567 |
539 |
|
|
| 568 |
|
- |
let event = make_event(
|
| 569 |
|
- |
EventType::CustomerSubscriptionUpdated,
|
| 570 |
|
- |
EventObject::Subscription(sub),
|
| 571 |
|
- |
);
|
| 572 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
540 |
+ |
let resp = post_event_json(&mut h, "customer.subscription.updated", sub).await;
|
| 573 |
541 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 574 |
542 |
|
|
| 575 |
543 |
|
// Verify status changed
|
| 604 |
572 |
|
let stripe_sub_id = "sub_test_inv_success";
|
| 605 |
573 |
|
insert_active_subscription(&h, &fix, stripe_sub_id).await;
|
| 606 |
574 |
|
|
| 607 |
|
- |
// Build invoice with subscription reference and period
|
| 608 |
|
- |
let invoice: stripe::Invoice = serde_json::from_value(serde_json::json!({
|
|
575 |
+ |
let invoice = serde_json::json!({
|
| 609 |
576 |
|
"id": "in_test_success_001",
|
|
577 |
+ |
"object": "invoice",
|
| 610 |
578 |
|
"subscription": stripe_sub_id,
|
| 611 |
579 |
|
"period_start": 1702592000,
|
| 612 |
580 |
|
"period_end": 1705184000,
|
| 613 |
581 |
|
"billing_reason": "subscription_cycle",
|
| 614 |
582 |
|
"livemode": false,
|
| 615 |
|
- |
}))
|
| 616 |
|
- |
.expect("Invoice deserialization");
|
|
583 |
+ |
});
|
| 617 |
584 |
|
|
| 618 |
|
- |
let event = make_event(
|
| 619 |
|
- |
EventType::InvoicePaymentSucceeded,
|
| 620 |
|
- |
EventObject::Invoice(invoice),
|
| 621 |
|
- |
);
|
| 622 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
585 |
+ |
let resp = post_event_json(&mut h, "invoice.payment_succeeded", invoice).await;
|
| 623 |
586 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 624 |
587 |
|
|
| 625 |
588 |
|
// Verify subscription period was updated
|
| 644 |
607 |
|
let stripe_sub_id = "sub_test_inv_failed";
|
| 645 |
608 |
|
insert_active_subscription(&h, &fix, stripe_sub_id).await;
|
| 646 |
609 |
|
|
| 647 |
|
- |
// Build invoice with subscription reference
|
| 648 |
|
- |
let invoice: stripe::Invoice = serde_json::from_value(serde_json::json!({
|
|
610 |
+ |
let invoice = serde_json::json!({
|
| 649 |
611 |
|
"id": "in_test_failed_001",
|
|
612 |
+ |
"object": "invoice",
|
| 650 |
613 |
|
"subscription": stripe_sub_id,
|
|
614 |
+ |
"period_start": 1700000000,
|
|
615 |
+ |
"period_end": 1702592000,
|
| 651 |
616 |
|
"livemode": false,
|
| 652 |
|
- |
}))
|
| 653 |
|
- |
.expect("Invoice deserialization");
|
|
617 |
+ |
});
|
| 654 |
618 |
|
|
| 655 |
|
- |
let event = make_event(
|
| 656 |
|
- |
EventType::InvoicePaymentFailed,
|
| 657 |
|
- |
EventObject::Invoice(invoice),
|
| 658 |
|
- |
);
|
| 659 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
619 |
+ |
let resp = post_event_json(&mut h, "invoice.payment_failed", invoice).await;
|
| 660 |
620 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 661 |
621 |
|
|
| 662 |
622 |
|
// Verify status changed to past_due
|
| 684 |
644 |
|
.unwrap();
|
| 685 |
645 |
|
|
| 686 |
646 |
|
// Only details_submitted is true; charges and payouts still false
|
| 687 |
|
- |
let account: stripe::Account = serde_json::from_value(serde_json::json!({
|
|
647 |
+ |
let account = serde_json::json!({
|
| 688 |
648 |
|
"id": acct_id,
|
|
649 |
+ |
"object": "account",
|
| 689 |
650 |
|
"charges_enabled": false,
|
| 690 |
651 |
|
"payouts_enabled": false,
|
| 691 |
652 |
|
"details_submitted": true,
|
| 692 |
|
- |
}))
|
| 693 |
|
- |
.unwrap();
|
|
653 |
+ |
});
|
| 694 |
654 |
|
|
| 695 |
|
- |
let event = make_event(EventType::AccountUpdated, EventObject::Account(account));
|
| 696 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
655 |
+ |
let resp = post_event_json(&mut h, "account.updated", account).await;
|
| 697 |
656 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 698 |
657 |
|
|
| 699 |
658 |
|
let (charges, payouts, onboarding): (bool, bool, bool) = sqlx::query_as(
|
| 714 |
673 |
|
let mut h = TestHarness::with_stripe().await;
|
| 715 |
674 |
|
|
| 716 |
675 |
|
// No user has this stripe_account_id
|
| 717 |
|
- |
let account: stripe::Account = serde_json::from_value(serde_json::json!({
|
|
676 |
+ |
let account = serde_json::json!({
|
| 718 |
677 |
|
"id": "acct_nonexistent",
|
|
678 |
+ |
"object": "account",
|
| 719 |
679 |
|
"charges_enabled": true,
|
| 720 |
680 |
|
"payouts_enabled": true,
|
| 721 |
681 |
|
"details_submitted": true,
|
| 722 |
|
- |
}))
|
| 723 |
|
- |
.unwrap();
|
|
682 |
+ |
});
|
| 724 |
683 |
|
|
| 725 |
|
- |
let event = make_event(EventType::AccountUpdated, EventObject::Account(account));
|
| 726 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
684 |
+ |
let resp = post_event_json(&mut h, "account.updated", account).await;
|
| 727 |
685 |
|
assert_eq!(
|
| 728 |
686 |
|
resp.status.as_u16(),
|
| 729 |
687 |
|
200,
|
| 791 |
749 |
|
meta.insert("buyer_id".to_string(), buyer_id.to_string());
|
| 792 |
750 |
|
meta.insert("seller_id".to_string(), seller_id.to_string());
|
| 793 |
751 |
|
meta.insert("item_id".to_string(), item_id.clone());
|
| 794 |
|
- |
stripe::CheckoutSession {
|
| 795 |
|
- |
id: session_id.parse().unwrap(),
|
| 796 |
|
- |
metadata: Some(meta),
|
| 797 |
|
- |
payment_intent: Some(serde_json::from_value(serde_json::json!("pi_test_idem_001")).unwrap()),
|
| 798 |
|
- |
..Default::default()
|
| 799 |
|
- |
}
|
|
752 |
+ |
serde_json::json!({
|
|
753 |
+ |
"id": session_id,
|
|
754 |
+ |
"object": "checkout_session",
|
|
755 |
+ |
"mode": "payment",
|
|
756 |
+ |
"metadata": meta,
|
|
757 |
+ |
"payment_intent": "pi_test_idem_001",
|
|
758 |
+ |
})
|
| 800 |
759 |
|
};
|
| 801 |
760 |
|
|
| 802 |
761 |
|
// First event
|
| 803 |
|
- |
let event1 = make_event(
|
| 804 |
|
- |
EventType::CheckoutSessionCompleted,
|
| 805 |
|
- |
EventObject::CheckoutSession(build_session()),
|
| 806 |
|
- |
);
|
| 807 |
|
- |
let resp = post_event(&mut h, &event1).await;
|
|
762 |
+ |
let resp = post_event_json(&mut h, "checkout.session.completed", build_session()).await;
|
| 808 |
763 |
|
assert_eq!(resp.status.as_u16(), 200, "First webhook failed: {}", resp.text);
|
| 809 |
764 |
|
|
| 810 |
765 |
|
// Second event (duplicate)
|
| 811 |
|
- |
let mut event2 = make_event(
|
| 812 |
|
- |
EventType::CheckoutSessionCompleted,
|
| 813 |
|
- |
EventObject::CheckoutSession(build_session()),
|
| 814 |
|
- |
);
|
| 815 |
|
- |
event2.id = "evt_test_002".parse().unwrap();
|
| 816 |
|
- |
let resp = post_event(&mut h, &event2).await;
|
|
766 |
+ |
let resp = post_event_json_with_id(&mut h, "evt_test_002", "checkout.session.completed", build_session()).await;
|
| 817 |
767 |
|
assert_eq!(resp.status.as_u16(), 200, "Duplicate webhook should succeed: {}", resp.text);
|
| 818 |
768 |
|
|
| 819 |
769 |
|
// Verify still one completed transaction
|
| 934 |
884 |
|
// fallback path was exercised.
|
| 935 |
885 |
|
// ---------------------------------------------------------------------------
|
| 936 |
886 |
|
|
| 937 |
|
- |
fn make_subscription(stripe_sub_id: &str, status: &str) -> stripe::Subscription {
|
| 938 |
|
- |
serde_json::from_value(serde_json::json!({
|
|
887 |
+ |
fn make_subscription(stripe_sub_id: &str, status: &str) -> serde_json::Value {
|
|
888 |
+ |
serde_json::json!({
|
| 939 |
889 |
|
"id": stripe_sub_id,
|
|
890 |
+ |
"object": "subscription",
|
| 940 |
891 |
|
"status": status,
|
| 941 |
|
- |
"customer": "cus_fan_plus_test",
|
| 942 |
|
- |
"currency": "usd",
|
| 943 |
892 |
|
"cancel_at_period_end": false,
|
| 944 |
893 |
|
"items": {
|
| 945 |
894 |
|
"object": "list",
|
| 946 |
|
- |
"data": [],
|
| 947 |
|
- |
"has_more": false,
|
| 948 |
|
- |
"url": "/v1/subscription_items"
|
|
895 |
+ |
"data": [{
|
|
896 |
+ |
"id": "si_fan_plus_test",
|
|
897 |
+ |
"object": "subscription_item",
|
|
898 |
+ |
"subscription": stripe_sub_id,
|
|
899 |
+ |
"current_period_start": 1700000000_i64,
|
|
900 |
+ |
"current_period_end": 1702592000_i64,
|
|
901 |
+ |
"metadata": {},
|
|
902 |
+ |
}],
|
| 949 |
903 |
|
},
|
| 950 |
|
- |
"automatic_tax": { "enabled": false },
|
| 951 |
|
- |
"billing_cycle_anchor": 1700000000_i64,
|
| 952 |
|
- |
"current_period_start": 1700000000_i64,
|
| 953 |
|
- |
"current_period_end": 1702592000_i64,
|
| 954 |
|
- |
"created": 1700000000_i64,
|
| 955 |
|
- |
"start_date": 1700000000_i64,
|
| 956 |
|
- |
"livemode": false,
|
| 957 |
|
- |
"metadata": {},
|
| 958 |
|
- |
}))
|
| 959 |
|
- |
.expect("Subscription deserialization")
|
|
904 |
+ |
})
|
| 960 |
905 |
|
}
|
| 961 |
906 |
|
|
| 962 |
907 |
|
#[tokio::test]
|
| 977 |
922 |
|
.unwrap();
|
| 978 |
923 |
|
|
| 979 |
924 |
|
let sub = make_subscription(stripe_sub_id, "past_due");
|
| 980 |
|
- |
let event = make_event(EventType::CustomerSubscriptionUpdated, EventObject::Subscription(sub));
|
| 981 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
925 |
+ |
let resp = post_event_json(&mut h, "customer.subscription.updated", sub).await;
|
| 982 |
926 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 983 |
927 |
|
|
| 984 |
928 |
|
// fan_plus row reflects the new status — pins that the fan_plus branch
|
| 1011 |
955 |
|
.unwrap();
|
| 1012 |
956 |
|
|
| 1013 |
957 |
|
let sub = make_subscription(stripe_sub_id, "canceled");
|
| 1014 |
|
- |
let event = make_event(EventType::CustomerSubscriptionDeleted, EventObject::Subscription(sub));
|
| 1015 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
958 |
+ |
let resp = post_event_json(&mut h, "customer.subscription.deleted", sub).await;
|
| 1016 |
959 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 1017 |
960 |
|
|
| 1018 |
961 |
|
// Pins that `cancel_fan_plus` ran. We don't pin the exact column the
|
| 1029 |
972 |
|
assert!(!active, "Fan+ subscription must not be active after cancellation");
|
| 1030 |
973 |
|
}
|
| 1031 |
974 |
|
|
| 1032 |
|
- |
fn make_invoice(stripe_sub_id: &str, billing_reason: &str) -> stripe::Invoice {
|
| 1033 |
|
- |
serde_json::from_value(serde_json::json!({
|
|
975 |
+ |
fn make_invoice(stripe_sub_id: &str, billing_reason: &str) -> serde_json::Value {
|
|
976 |
+ |
serde_json::json!({
|
| 1034 |
977 |
|
"id": "in_test_fp",
|
| 1035 |
978 |
|
"object": "invoice",
|
| 1036 |
979 |
|
"subscription": stripe_sub_id,
|
| 1039 |
982 |
|
"period_end": 1702592000_i64,
|
| 1040 |
983 |
|
"currency": "usd",
|
| 1041 |
984 |
|
"livemode": false,
|
| 1042 |
|
- |
}))
|
| 1043 |
|
- |
.expect("Invoice deserialization")
|
|
985 |
+ |
})
|
| 1044 |
986 |
|
}
|
| 1045 |
987 |
|
|
| 1046 |
988 |
|
#[tokio::test]
|
| 1062 |
1004 |
|
|
| 1063 |
1005 |
|
// billing_reason != "subscription_cycle" → not a renewal, just updates period.
|
| 1064 |
1006 |
|
let invoice = make_invoice(stripe_sub_id, "subscription_create");
|
| 1065 |
|
- |
let event = make_event(EventType::InvoicePaymentSucceeded, EventObject::Invoice(invoice));
|
| 1066 |
|
- |
let resp = post_event(&mut h, &event).await;
|
|
1007 |
+ |
let resp = post_event_json(&mut h, "invoice.payment_succeeded", invoice).await;
|
| 1067 |
1008 |
|
assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
|
| 1068 |
1009 |
|
|