paykit-lib 0.1.0

Paykit library implementation
Documentation
use super::*;

fn receipt_access_json(access: &ReceiptAccess) -> String {
    let mut object = serde_json::Map::new();
    object.insert("version".to_string(), serde_json::json!(access.version));
    object.insert(
        "kind".to_string(),
        serde_json::json!("paykit.receipt_access"),
    );
    object.insert(
        "app_id".to_string(),
        serde_json::json!(test_app_id().as_str()),
    );
    object.insert(
        "event_id".to_string(),
        serde_json::json!(access.event_id.as_str()),
    );
    object.insert(
        "receipt_id".to_string(),
        serde_json::json!(access.receipt_id.as_str()),
    );
    object.insert(
        "payment_reference".to_string(),
        serde_json::json!(access.payment_reference.as_str()),
    );
    if let Some(payment_request_id) = &access.payment_request_id {
        object.insert(
            "payment_request_id".to_string(),
            serde_json::json!(payment_request_id.as_str()),
        );
    }
    if let Some(period) = &access.billing_period {
        object.insert(
            "billing_period".to_string(),
            serde_json::json!({
                "starts_at": period.starts_at(),
                "ends_at": period.ends_at(),
            }),
        );
    }
    object.insert("location".to_string(), serde_json::json!(&access.location));
    object.insert("key".to_string(), serde_json::json!(access.key.as_str()));
    serde_json::Value::Object(object).to_string()
}

#[tokio::test]
async fn prepared_receipt_can_be_stored_and_sent_in_retryable_steps() {
    let mut setup = PrivateTestSetup::new().await;
    let reference = PaymentReference::new("invoice-2026-0001").unwrap();
    let draft = ReceiptDraft {
        receipt_id: None,
        payment_reference: reference.clone(),
        payment_request_id: None,
        billing_period: None,
        payment_endpoint_identifier: Some(PaymentEndpointIdentifier::new("lightning").unwrap()),
        amount: Some(PaymentAmount::new("2500", "sats").unwrap()),
        metadata: serde_json::json!({
            "settlement_id": "abc-123",
            "details": { "confirmations": 3 }
        })
        .as_object()
        .cloned()
        .unwrap(),
    };

    let prepared = prepare_receipt(&setup.sender_link, draft).unwrap();
    assert_eq!(prepared.access.payment_reference, reference);
    assert_eq!(prepared.receipt.receipt_id, prepared.access.receipt_id);
    assert_eq!(
        prepared.access.location,
        ReceiptAccess::location_for(&prepared.access.receipt_id)
    );

    store_prepared_receipt(&setup.sender_session, &prepared)
        .await
        .unwrap();

    let stored = setup
        .sender_session
        .storage()
        .get(prepared.access.location.clone())
        .await
        .unwrap()
        .text()
        .await
        .unwrap();
    let receipt =
        decrypt_receipt(&stored, &prepared.access.key, &prepared.access.location).unwrap();
    assert_eq!(receipt, prepared.receipt);

    send_receipt_access(&mut setup.sender_link, &test_app_id(), &prepared.access)
        .await
        .unwrap();
    let access = receive_receipt_access_for_test(&mut setup.receiver_link).await;

    assert_eq!(access, vec![prepared.access]);
}

#[tokio::test]
async fn receipt_access_parser_returns_all_available_receipts_in_fifo_order() {
    let mut setup = PrivateTestSetup::new().await;
    let first_receipt_id = ReceiptId::new("450e8400-e29b-41d4-a716-446655440000").unwrap();
    let second_receipt_id = ReceiptId::new("650e8400-e29b-41d4-a716-446655440000").unwrap();
    let first_reference = PaymentReference::new("550e8400-e29b-41d4-a716-446655440000").unwrap();
    let second_reference = PaymentReference::new("650e8400-e29b-41d4-a716-446655440000").unwrap();
    let first_access = ReceiptAccess {
        version: 1,
        kind: PrivateMessageKind::ReceiptAccess,
        event_id: EventId::new("8a0d8b4c-913f-4e31-9f2c-2a6f5bb4d109").unwrap(),
        receipt_id: first_receipt_id.clone(),
        payment_request_id: None,
        billing_period: None,
        location: ReceiptAccess::location_for(&first_receipt_id),
        key: ReceiptDecryptionKey::generate(),
        payment_reference: first_reference.clone(),
    };
    let second_access = ReceiptAccess {
        version: 1,
        kind: PrivateMessageKind::ReceiptAccess,
        event_id: EventId::new("8a0d8b4c-913f-4e31-9f2c-2a6f5bb4d10a").unwrap(),
        receipt_id: second_receipt_id.clone(),
        payment_request_id: None,
        billing_period: None,
        location: ReceiptAccess::location_for(&second_receipt_id),
        key: ReceiptDecryptionKey::generate(),
        payment_reference: second_reference.clone(),
    };

    let first_json = receipt_access_json(&first_access);
    let second_json = receipt_access_json(&second_access);
    send_raw_private_application_message(&mut setup.sender_link, &first_json).await;
    send_raw_private_application_message(&mut setup.sender_link, &second_json).await;

    let received = receive_receipt_access_for_test(&mut setup.receiver_link).await;
    let empty = receive_receipt_access_for_test(&mut setup.receiver_link).await;

    assert_eq!(received.len(), 2);
    assert_eq!(received[0].receipt_id, first_receipt_id);
    assert_eq!(received[0].payment_reference, first_reference);
    assert_eq!(received[1].receipt_id, second_receipt_id);
    assert_eq!(received[1].payment_reference, second_reference);
    assert!(empty.is_empty());
}

#[tokio::test]
async fn receipt_access_parser_preserves_valid_receipts_when_one_message_is_malformed() {
    let mut setup = PrivateTestSetup::new().await;
    let first_receipt_id = ReceiptId::new("450e8400-e29b-41d4-a716-446655440000").unwrap();
    let second_receipt_id = ReceiptId::new("650e8400-e29b-41d4-a716-446655440000").unwrap();
    let malformed_receipt_id = ReceiptId::new("750e8400-e29b-41d4-a716-446655440000").unwrap();
    let first_reference = PaymentReference::new("550e8400-e29b-41d4-a716-446655440000").unwrap();
    let second_reference = PaymentReference::new("650e8400-e29b-41d4-a716-446655440000").unwrap();
    let malformed_reference =
        PaymentReference::new("750e8400-e29b-41d4-a716-446655440000").unwrap();
    let first_access = ReceiptAccess {
        version: 1,
        kind: PrivateMessageKind::ReceiptAccess,
        event_id: EventId::new("8a0d8b4c-913f-4e31-9f2c-2a6f5bb4d10b").unwrap(),
        receipt_id: first_receipt_id.clone(),
        payment_request_id: None,
        billing_period: None,
        location: ReceiptAccess::location_for(&first_receipt_id),
        key: ReceiptDecryptionKey::generate(),
        payment_reference: first_reference.clone(),
    };
    let malformed_access = ReceiptAccess {
        version: 1,
        kind: PrivateMessageKind::ReceiptAccess,
        event_id: EventId::new("8a0d8b4c-913f-4e31-9f2c-2a6f5bb4d10c").unwrap(),
        receipt_id: malformed_receipt_id,
        payment_request_id: None,
        billing_period: None,
        location: ReceiptAccess::location_for(&ReceiptId::new_v4()),
        key: ReceiptDecryptionKey::generate(),
        payment_reference: malformed_reference,
    };
    let second_access = ReceiptAccess {
        version: 1,
        kind: PrivateMessageKind::ReceiptAccess,
        event_id: EventId::new("8a0d8b4c-913f-4e31-9f2c-2a6f5bb4d10d").unwrap(),
        receipt_id: second_receipt_id.clone(),
        payment_request_id: None,
        billing_period: None,
        location: ReceiptAccess::location_for(&second_receipt_id),
        key: ReceiptDecryptionKey::generate(),
        payment_reference: second_reference.clone(),
    };

    let first_json = receipt_access_json(&first_access);
    let malformed_json = receipt_access_json(&malformed_access);
    let second_json = receipt_access_json(&second_access);
    send_raw_private_application_message(&mut setup.sender_link, &first_json).await;
    send_raw_private_application_message(&mut setup.sender_link, &malformed_json).await;
    send_raw_private_application_message(&mut setup.sender_link, &second_json).await;

    let received = receive_receipt_access_for_test(&mut setup.receiver_link).await;
    let empty = receive_receipt_access_for_test(&mut setup.receiver_link).await;

    assert_eq!(received.len(), 2);
    assert_eq!(received[0].receipt_id, first_receipt_id);
    assert_eq!(received[0].payment_reference, first_reference);
    assert_eq!(received[1].receipt_id, second_receipt_id);
    assert_eq!(received[1].payment_reference, second_reference);
    assert!(empty.is_empty());
}