adk-payments 0.5.0

Protocol-neutral agentic commerce and payment orchestration for ADK-Rust
Documentation
use serde_json::Value;

use crate::AP2_ALPHA_BASELINE;
use crate::domain::{
    Cart, CartLine, FulfillmentKind, FulfillmentSelection, Money, OrderSnapshot, OrderState,
    PaymentMethodSelection, PriceAdjustment, PriceAdjustmentKind, ProtocolDescriptor,
    ProtocolExtensionEnvelope, ProtocolExtensions, ReceiptState, TransactionRecord,
    TransactionState,
};
use crate::kernel::{
    CommerceContext, CreateCheckoutCommand, ExecutePaymentCommand, PaymentExecutionOutcome,
    SyncPaymentOutcomeCommand, UpdateCheckoutCommand,
};
use crate::protocol::ap2::types::{
    CartMandate, IntentMandate, PaymentMandate, PaymentReceipt, PaymentStatusEnvelope,
};

pub(crate) fn ap2_descriptor() -> ProtocolDescriptor {
    ProtocolDescriptor::ap2(AP2_ALPHA_BASELINE)
}

pub(crate) fn merge_extensions(
    mut left: ProtocolExtensions,
    right: ProtocolExtensions,
) -> ProtocolExtensions {
    for envelope in right.0 {
        left.push(envelope);
    }
    left
}

pub(crate) fn placeholder_cart_from_intent(intent: &IntentMandate) -> Cart {
    Cart {
        cart_id: None,
        lines: vec![CartLine {
            line_id: "intent".to_string(),
            merchant_sku: intent.skus.as_ref().and_then(|skus| skus.first().cloned()),
            title: "intent authorization".to_string(),
            quantity: 1,
            unit_price: Money::new("XXX", 0, 2),
            total_price: Money::new("XXX", 0, 2),
            product_class: Some("intent".to_string()),
            extensions: ProtocolExtensions::default(),
        }],
        subtotal: Some(Money::new("XXX", 0, 2)),
        adjustments: Vec::new(),
        total: Money::new("XXX", 0, 2),
        affiliate_attribution: None,
        extensions: ProtocolExtensions::default(),
    }
}

pub(crate) fn cart_from_cart_mandate(mandate: &CartMandate) -> Cart {
    let display_items = &mandate.contents.payment_request.details.display_items;
    let mut lines = Vec::with_capacity(display_items.len());
    let mut subtotal_minor = 0_i64;

    for (index, item) in display_items.iter().enumerate() {
        let line_total = item.amount.to_money();
        subtotal_minor = subtotal_minor.saturating_add(line_total.amount_minor);
        lines.push(CartLine {
            line_id: format!("{}:{index}", mandate.contents.id),
            merchant_sku: None,
            title: item.label.clone(),
            quantity: 1,
            unit_price: line_total.clone(),
            total_price: line_total,
            product_class: None,
            extensions: ProtocolExtensions::default(),
        });
    }

    let total = mandate.contents.payment_request.details.total.amount.to_money();
    let mut adjustments = Vec::new();
    if let Some(options) = &mandate.contents.payment_request.details.shipping_options {
        for option in options.iter().filter(|option| option.selected) {
            adjustments.push(PriceAdjustment {
                adjustment_id: option.id.clone(),
                kind: PriceAdjustmentKind::Shipping,
                label: option.label.clone(),
                amount: option.amount.to_money(),
                extensions: ProtocolExtensions::default(),
            });
        }
    }

    if let Some(modifiers) = &mandate.contents.payment_request.details.modifiers {
        for modifier in modifiers {
            if let Some(items) = &modifier.additional_display_items {
                for (index, item) in items.iter().enumerate() {
                    adjustments.push(PriceAdjustment {
                        adjustment_id: format!("{}:{index}", modifier.supported_methods),
                        kind: PriceAdjustmentKind::Fee,
                        label: item.label.clone(),
                        amount: item.amount.to_money(),
                        extensions: ProtocolExtensions::default(),
                    });
                }
            }
        }
    }

    let allocated_minor = subtotal_minor
        .saturating_add(adjustments.iter().map(|adjustment| adjustment.amount.amount_minor).sum());
    if allocated_minor != total.amount_minor {
        adjustments.push(PriceAdjustment {
            adjustment_id: "ap2_unallocated_delta".to_string(),
            kind: PriceAdjustmentKind::Other("ap2".to_string()),
            label: "AP2 total reconciliation".to_string(),
            amount: Money::new(
                total.currency.clone(),
                total.amount_minor.saturating_sub(allocated_minor),
                total.scale,
            ),
            extensions: ProtocolExtensions::default(),
        });
    }

    Cart {
        cart_id: Some(mandate.contents.id.clone()),
        lines,
        subtotal: Some(Money::new(total.currency.clone(), subtotal_minor, total.scale)),
        adjustments,
        total,
        affiliate_attribution: None,
        extensions: ProtocolExtensions::default(),
    }
}

pub(crate) fn fulfillment_from_cart_mandate(mandate: &CartMandate) -> Option<FulfillmentSelection> {
    mandate
        .contents
        .payment_request
        .details
        .shipping_options
        .as_ref()
        .and_then(|options| options.iter().find(|option| option.selected))
        .map(|option| FulfillmentSelection {
            fulfillment_id: option.id.clone(),
            kind: FulfillmentKind::Shipping,
            label: option.label.clone(),
            amount: Some(option.amount.to_money()),
            destination: None,
            requires_user_selection: mandate
                .contents
                .payment_request
                .options
                .as_ref()
                .is_some_and(|options| options.request_shipping),
            extensions: ProtocolExtensions::default(),
        })
}

pub(crate) fn intent_create_checkout_command(
    intent: &IntentMandate,
    context: CommerceContext,
) -> CreateCheckoutCommand {
    CreateCheckoutCommand { context, cart: placeholder_cart_from_intent(intent), fulfillment: None }
}

pub(crate) fn cart_create_checkout_command(
    mandate: &CartMandate,
    context: CommerceContext,
) -> CreateCheckoutCommand {
    CreateCheckoutCommand {
        context,
        cart: cart_from_cart_mandate(mandate),
        fulfillment: fulfillment_from_cart_mandate(mandate),
    }
}

pub(crate) fn cart_update_checkout_command(
    mandate: &CartMandate,
    context: CommerceContext,
) -> UpdateCheckoutCommand {
    UpdateCheckoutCommand {
        context,
        cart: Some(cart_from_cart_mandate(mandate)),
        fulfillment: fulfillment_from_cart_mandate(mandate),
    }
}

pub(crate) fn payment_method_selection(mandate: &PaymentMandate) -> PaymentMethodSelection {
    let reference = mandate
        .payment_mandate_contents
        .payment_response
        .details
        .as_ref()
        .and_then(|details| details.get("token"))
        .and_then(Value::as_object)
        .and_then(|token| token.get("id"))
        .and_then(Value::as_str)
        .map(str::to_string);

    PaymentMethodSelection {
        selection_kind: mandate.payment_mandate_contents.payment_response.method_name.clone(),
        reference,
        display_hint: None,
        extensions: ProtocolExtensions::default(),
    }
}

pub(crate) fn execute_payment_command(
    mandate: &PaymentMandate,
    context: CommerceContext,
    supporting_evidence_refs: Vec<crate::domain::EvidenceReference>,
) -> ExecutePaymentCommand {
    ExecutePaymentCommand {
        context,
        amount: mandate.payment_mandate_contents.payment_details_total.amount.to_money(),
        selected_payment_method: Some(payment_method_selection(mandate)),
        supporting_evidence_refs,
        extensions: ProtocolExtensions::default(),
    }
}

pub(crate) fn sync_payment_outcome_command(
    record: Option<&TransactionRecord>,
    receipt: &PaymentReceipt,
    context: CommerceContext,
) -> SyncPaymentOutcomeCommand {
    let outcome = match receipt.payment_status {
        PaymentStatusEnvelope::Success(_) => PaymentExecutionOutcome::Completed,
        PaymentStatusEnvelope::Error(_) | PaymentStatusEnvelope::Failure(_) => {
            PaymentExecutionOutcome::Failed
        }
    };
    let order_state = match outcome {
        PaymentExecutionOutcome::Completed => OrderState::Completed,
        PaymentExecutionOutcome::Failed => OrderState::Failed,
        PaymentExecutionOutcome::Authorized | PaymentExecutionOutcome::InterventionRequired => {
            OrderState::Authorized
        }
    };
    let receipt_state = match outcome {
        PaymentExecutionOutcome::Completed => ReceiptState::Settled,
        PaymentExecutionOutcome::Failed => ReceiptState::Failed,
        PaymentExecutionOutcome::Authorized => ReceiptState::Authorized,
        PaymentExecutionOutcome::InterventionRequired => ReceiptState::Pending,
    };

    SyncPaymentOutcomeCommand {
        context,
        outcome,
        order: Some(OrderSnapshot {
            order_id: record
                .and_then(|record| record.order.as_ref().and_then(|order| order.order_id.clone()))
                .or_else(|| Some(receipt.payment_mandate_id.clone())),
            receipt_id: Some(receipt.payment_id.clone()),
            state: order_state,
            receipt_state,
            extensions: ProtocolExtensions::default(),
        }),
        intervention: None,
        generated_evidence_refs: Vec::new(),
    }
}

pub(crate) fn update_record_extensions(
    record: &mut TransactionRecord,
    envelope: ProtocolExtensionEnvelope,
) {
    if !record.extensions.as_slice().contains(&envelope) {
        record.attach_extension(envelope);
    }
}

pub(crate) fn update_record_state_from_receipt(
    record: &mut TransactionRecord,
    receipt: &PaymentReceipt,
) {
    if record.order.is_none() {
        record.order = Some(OrderSnapshot {
            order_id: Some(receipt.payment_mandate_id.clone()),
            receipt_id: Some(receipt.payment_id.clone()),
            state: OrderState::Draft,
            receipt_state: ReceiptState::Pending,
            extensions: ProtocolExtensions::default(),
        });
    }

    if let Some(order) = &mut record.order {
        order.receipt_id = Some(receipt.payment_id.clone());
        match receipt.payment_status {
            PaymentStatusEnvelope::Success(_) => {
                order.state = OrderState::Completed;
                order.receipt_state = ReceiptState::Settled;
            }
            PaymentStatusEnvelope::Error(_) | PaymentStatusEnvelope::Failure(_) => {
                order.state = OrderState::Failed;
                order.receipt_state = ReceiptState::Failed;
            }
        }
    }

    match receipt.payment_status {
        PaymentStatusEnvelope::Success(_) => {
            record.state = TransactionState::Completed;
        }
        PaymentStatusEnvelope::Error(_) | PaymentStatusEnvelope::Failure(_) => {
            record.state = TransactionState::Failed;
        }
    }
}