mod common;
use common::*;
use backbone_integrations::application::service::integrations_write_service::*;
use serde_json::json;
use uuid::Uuid;
async fn connector(svc: &IntegrationsWriteService, provider: &str) -> Uuid {
svc.register_connector(NewConnector {
provider: provider.into(), kind: "payment_gateway".into(), direction: "inbound".into(),
}).await.unwrap()
}
fn event(company: Uuid, conn: Uuid, ext: &str) -> InboundEvent {
InboundEvent {
company_id: company, connector_id: conn, event_type: "payment_settled".into(), external_id: ext.into(), business_key: ext.into(),
raw: "{}".into(), payload: json!({"amount": "1000", "customer_id": Uuid::new_v4().to_string()}),
}
}
#[tokio::test]
async fn iip1_external_id_required() {
let pool = pool().await;
let company = Uuid::new_v4();
let svc = module(pool.clone()).await.integrations_write_service.clone();
let conn = connector(&svc, &format!("p-{}", Uuid::new_v4())).await;
let r = svc.receive_event(event(company, conn, " "), &FakeTarget::new(), &CapturingSink::new()).await;
assert!(matches!(r, Err(IntegrationError::Invalid(_))));
}
#[tokio::test]
async fn iip2_inactive_connector_rejected() {
let pool = pool().await;
let company = Uuid::new_v4();
let svc = module(pool.clone()).await.integrations_write_service.clone();
let conn = connector(&svc, &format!("p-{}", Uuid::new_v4())).await;
sqlx::query("UPDATE integrations.integration_connectors SET status='inactive' WHERE id=$1")
.bind(conn).execute(&pool).await.unwrap();
let r = svc.receive_event(event(company, conn, "n-x"), &FakeTarget::new(), &CapturingSink::new()).await;
assert!(matches!(r, Err(IntegrationError::InvalidState(_))));
}
#[tokio::test]
async fn iip3_dedup_is_per_connector() {
let pool = pool().await;
let company = Uuid::new_v4();
let svc = module(pool.clone()).await.integrations_write_service.clone();
let a = connector(&svc, &format!("midtrans-{}", Uuid::new_v4())).await;
let b = connector(&svc, &format!("xendit-{}", Uuid::new_v4())).await;
let target = FakeTarget::new();
let ra = svc.receive_event(event(company, a, "TXN-42"), &target, &CapturingSink::new()).await.unwrap();
let rb = svc.receive_event(event(company, b, "TXN-42"), &target, &CapturingSink::new()).await.unwrap();
assert!(!ra.duplicate);
assert!(!rb.duplicate, "the same external id on a different connector is a distinct event");
assert_ne!(ra.event_id, rb.event_id);
assert_eq!(target.map_count(), 2);
}
#[tokio::test]
async fn iip4_lifecycle_event_durable() {
let pool = pool().await;
let company = Uuid::new_v4();
let svc = module(pool.clone()).await.integrations_write_service.clone();
let conn = connector(&svc, &format!("p-{}", Uuid::new_v4())).await;
let out = svc.receive_event(event(company, conn, &format!("n-{}", Uuid::new_v4())), &FakeTarget::new(), &DroppingSink).await.unwrap();
let staged: i64 = sqlx::query_scalar(
"SELECT count(*) FROM integrations.outbox_events WHERE aggregate_id=$1 AND event_type='IntegrationEventMapped'")
.bind(out.event_id.to_string()).fetch_one(&pool).await.unwrap();
assert_eq!(staged, 1, "IntegrationEventMapped durably staged despite the lost publish");
}
#[tokio::test]
async fn iip5_second_settled_notification_dedups() {
let pool = pool().await;
let company = Uuid::new_v4();
let svc = module(pool.clone()).await.integrations_write_service.clone();
let conn = connector(&svc, &format!("midtrans-{}", Uuid::new_v4())).await;
let target = FakeTarget::new();
let mk = |notif: &str| InboundEvent {
company_id: company, connector_id: conn, event_type: "payment_settled".into(),
external_id: notif.into(), business_key: "SO-9:settled".into(),
raw: "{}".into(), payload: json!({"amount": "1000", "customer_id": Uuid::new_v4().to_string()}),
};
let a = svc.receive_event(mk("notif-A"), &target, &CapturingSink::new()).await.unwrap();
let b = svc.receive_event(mk("notif-B"), &target, &CapturingSink::new()).await.unwrap();
assert!(!a.duplicate);
assert!(b.duplicate, "the second settled notification for the same order is a duplicate, not a new map");
assert_eq!(a.event_id, b.event_id);
assert_eq!(target.map_count(), 1, "the payment is applied ONCE for the order, not twice");
}