Migrate reconcile + tipping onto PaymentProvider trait; add worker tests
Two compat-path holdovers migrated:
- src/reconcile.rs: was state.btcpay_client().get_invoice() with
manual JSON parsing of BTCPay-specific status strings ("Settled",
"Complete", "Expired", "Invalid"). Now state.payment_provider()
.get_invoice_status() returning the typed ProviderInvoiceStatus
enum. The string normalization moves into BtcpayProvider's impl
where it belongs.
- src/tipping.rs: was state.btcpay_client().pay_lightning_invoice()
returning raw JSON, then manual paymentHash extraction. Now
provider.pay_lightning_invoice() returning a typed PaymentReceipt
{ payment_hash, raw }. The audit message now records the active
provider's kind() rather than hardcoding "BTCPay LN node".
Combined with v0.1.0:43's purchase migration, the daemon's
non-test code now contains zero calls to state.btcpay_client() or
.btcpay_webhook_secret(). Those compat accessors stay on AppState
for v0.2 (no need to break things gratuitously) but they're dead
code in the production path. Zaprite's drop-in only needs to
implement the trait.
Worker integration tests (tests/worker.rs):
- worker_marks_failure_and_schedules_retry_on_500: spins up a tiny
axum receiver that 500s, calls webhooks::tick(), verifies attempt
count and next-attempt scheduling.
- worker_dead_letters_after_max_attempts: seeds a row at attempt
count 9, ticks once, verifies attempt_count → 10 and
next_attempt_at → NULL. Confirms the row also satisfies the admin
DLQ predicate (the contract :43's webhook_deliveries.rs depends
on).
- worker_marks_success_on_2xx: pins the happy path.
webhooks::tick is now `pub` so integration tests can drive it
synchronously.
Test count: 26 (9 unit + 4 migration + 10 API + 3 worker).
This commit is contained in:
@@ -199,11 +199,15 @@ async fn run_tip(
|
||||
}
|
||||
};
|
||||
|
||||
// Pay it via the operator's BTCPay Lightning node.
|
||||
let btcpay = match state.btcpay_client().await {
|
||||
Ok(c) => c,
|
||||
// Pay it via the active provider's LN node. Provider-agnostic;
|
||||
// BTCPay implements `pay_lightning_invoice` today, future
|
||||
// providers either implement it (Zaprite via Strike?) or fall
|
||||
// through to the trait default which returns a "not supported"
|
||||
// error that we record as a failed tip.
|
||||
let provider = match state.payment_provider().await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
let detail = format!("BTCPay client unavailable: {e:?}");
|
||||
let detail = format!("payment provider unavailable: {e:?}");
|
||||
repo::record_tip_attempt(
|
||||
&state.db,
|
||||
license_id,
|
||||
@@ -222,17 +226,13 @@ async fn run_tip(
|
||||
}
|
||||
};
|
||||
|
||||
match btcpay.pay_lightning_invoice(&invoice).await {
|
||||
Ok(payment) => {
|
||||
let payment_hash = payment
|
||||
.get("paymentHash")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
match provider.pay_lightning_invoice(&invoice).await {
|
||||
Ok(receipt) => {
|
||||
tracing::info!(
|
||||
license = %license_id,
|
||||
recipient = %recipient,
|
||||
amount_sats = tip_sats,
|
||||
payment_hash = ?payment_hash,
|
||||
payment_hash = ?receipt.payment_hash,
|
||||
"tip sent"
|
||||
);
|
||||
repo::record_tip_attempt(
|
||||
@@ -244,8 +244,12 @@ async fn run_tip(
|
||||
pct,
|
||||
label.as_deref(),
|
||||
"sent",
|
||||
Some(&format!("paid via BTCPay LN node ({} sats)", tip_sats)),
|
||||
payment_hash.as_deref(),
|
||||
Some(&format!(
|
||||
"paid via {} LN node ({} sats)",
|
||||
provider.kind().as_str(),
|
||||
tip_sats
|
||||
)),
|
||||
receipt.payment_hash.as_deref(),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
Reference in New Issue
Block a user