This commit is contained in:
spiro-alvin-nyasimi
2026-08-17 16:20:13 +03:00
parent c6a6755b08
commit cd186868fa
75 changed files with 3608 additions and 870 deletions

View File

@@ -0,0 +1,78 @@
package com.test.payment.models.audit;
import com.test.payment.models.PaymentProviderType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.math.BigDecimal;
import java.time.Instant;
/**
* An asynchronous result callback from Airtel Money, stored whole alongside the fields
* worth querying on, and tied back to the call that caused it. Unlike
* PAYMENT_CALLBACKS this keeps every callback, duplicates included — an audit trail
* that silently drops repeats is not one.
*/
@Entity
@Getter
@Setter
@ToString(exclude = "Request")
public class AirtelCallbackResponse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
/** Null when the callback could not be matched to an outbound request. */
@ManyToOne(targetEntity = AirtelRequest.class, fetch = FetchType.LAZY)
@JoinColumn(name = "Request")
private AirtelRequest Request;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentProviderType Provider;
@Column(length = 100)
private String ProviderReference;
@Column(length = 30)
private String ResultCode;
@Column(length = 255)
private String ResultDesc;
@Column(length = 50)
private String ReceiptNumber;
@Column(precision = 10, scale = 2)
private BigDecimal Amount;
@Column(length = 15)
private String PhoneNumber;
@Column(length = 20)
private String TransactionDate;
/** The callback exactly as it arrived. */
@Column(columnDefinition = "TEXT")
private String RawPayload;
/** False when no matching request could be found — an orphan worth alerting on. */
@Column(nullable = false)
private boolean Matched;
@Column(nullable = false)
private Instant CreatedAt;
}

View File

@@ -0,0 +1,68 @@
package com.test.payment.models.audit;
import com.test.payment.models.PaymentProviderType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Instant;
/**
* One outbound call to Airtel Money, recorded verbatim: what we sent, where, and on whose
* behalf. Written asynchronously by ProviderCallAudit — nothing on the payment path
* waits for this row.
*/
@Entity
@Getter
@Setter
@ToString
public class AirtelRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
/** Market-qualified provider, e.g. AIRTEL_KE. */
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentProviderType Provider;
/** Which call this was: USSD_PUSH, STATUS_QUERY, TOKEN. */
@Column(nullable = false, length = 40)
private String Operation;
/**
* The payment attempt this call belongs to; null for calls with no payment
* (token fetches). A string, not the numeric id, so it can carry a NanoID.
*/
@Column(length = 36)
private String InitiationId;
/**
* The provider's own reference once known, so a later callback can be tied back
* to the exact call that produced it.
*/
@Column(length = 100)
private String ProviderReference;
@Column(length = 10)
private String HttpMethod;
@Column(length = 512)
private String Url;
/** Outbound body as sent, JSON. Credentials are redacted before this is stored. */
@Column(columnDefinition = "TEXT")
private String RequestBody;
@Column(nullable = false)
private Instant CreatedAt;
}

View File

@@ -0,0 +1,52 @@
package com.test.payment.models.audit;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Instant;
/**
* What Airtel Money answered a AirtelRequest with — including the failures, which is
* usually what an audit is actually needed for.
*/
@Entity
@Getter
@Setter
@ToString(exclude = "Request")
public class AirtelResponse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@ManyToOne(targetEntity = AirtelRequest.class, fetch = FetchType.LAZY)
@JoinColumn(name = "Request", nullable = false)
private AirtelRequest Request;
/** HTTP status, or null when the call never got an answer (timeout, DNS, reset). */
private Integer HttpStatus;
/** Response body verbatim, JSON. */
@Column(columnDefinition = "TEXT")
private String ResponseBody;
/** Exception summary when the call failed outright. */
@Column(length = 512)
private String Error;
/** Round-trip time, for spotting Airtel Money degrading before it starts erroring. */
private Long DurationMs;
@Column(nullable = false)
private Instant CreatedAt;
}

View File

@@ -0,0 +1,78 @@
package com.test.payment.models.audit;
import com.test.payment.models.PaymentProviderType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.math.BigDecimal;
import java.time.Instant;
/**
* An asynchronous result callback from M-Pesa, stored whole alongside the fields
* worth querying on, and tied back to the call that caused it. Unlike
* PAYMENT_CALLBACKS this keeps every callback, duplicates included — an audit trail
* that silently drops repeats is not one.
*/
@Entity
@Getter
@Setter
@ToString(exclude = "Request")
public class MpesaCallbackResponse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
/** Null when the callback could not be matched to an outbound request. */
@ManyToOne(targetEntity = MpesaRequest.class, fetch = FetchType.LAZY)
@JoinColumn(name = "Request")
private MpesaRequest Request;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentProviderType Provider;
@Column(length = 100)
private String ProviderReference;
@Column(length = 30)
private String ResultCode;
@Column(length = 255)
private String ResultDesc;
@Column(length = 50)
private String ReceiptNumber;
@Column(precision = 10, scale = 2)
private BigDecimal Amount;
@Column(length = 15)
private String PhoneNumber;
@Column(length = 20)
private String TransactionDate;
/** The callback exactly as it arrived. */
@Column(columnDefinition = "TEXT")
private String RawPayload;
/** False when no matching request could be found — an orphan worth alerting on. */
@Column(nullable = false)
private boolean Matched;
@Column(nullable = false)
private Instant CreatedAt;
}

View File

@@ -0,0 +1,68 @@
package com.test.payment.models.audit;
import com.test.payment.models.PaymentProviderType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Instant;
/**
* One outbound call to M-Pesa, recorded verbatim: what we sent, where, and on whose
* behalf. Written asynchronously by ProviderCallAudit — nothing on the payment path
* waits for this row.
*/
@Entity
@Getter
@Setter
@ToString
public class MpesaRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
/** Market-qualified provider, e.g. MPESA_KE. */
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentProviderType Provider;
/** Which call this was: STK_PUSH, STK_QUERY, TOKEN. */
@Column(nullable = false, length = 40)
private String Operation;
/**
* The payment attempt this call belongs to; null for calls with no payment
* (token fetches). A string, not the numeric id, so it can carry a NanoID.
*/
@Column(length = 5000)
private String InitiationId;
/**
* The provider's own reference once known, so a later callback can be tied back
* to the exact call that produced it.
*/
@Column(length = 100)
private String ProviderReference;
@Column(length = 10)
private String HttpMethod;
@Column(length = 512)
private String Url;
/** Outbound body as sent, JSON. Credentials are redacted before this is stored. */
@Column(columnDefinition = "TEXT")
private String RequestBody;
@Column(nullable = false)
private Instant CreatedAt;
}

View File

@@ -0,0 +1,52 @@
package com.test.payment.models.audit;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Instant;
/**
* What M-Pesa answered a MpesaRequest with — including the failures, which is
* usually what an audit is actually needed for.
*/
@Entity
@Getter
@Setter
@ToString(exclude = "Request")
public class MpesaResponse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@ManyToOne(targetEntity = MpesaRequest.class, fetch = FetchType.LAZY)
@JoinColumn(name = "Request", nullable = false)
private MpesaRequest Request;
/** HTTP status, or null when the call never got an answer (timeout, DNS, reset). */
private Integer HttpStatus;
/** Response body verbatim, JSON. */
@Column(columnDefinition = "TEXT")
private String ResponseBody;
/** Exception summary when the call failed outright. */
@Column(length = 512)
private String Error;
/** Round-trip time, for spotting M-Pesa degrading before it starts erroring. */
private Long DurationMs;
@Column(nullable = false)
private Instant CreatedAt;
}

View File

@@ -0,0 +1,78 @@
package com.test.payment.models.audit;
import com.test.payment.models.PaymentProviderType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.math.BigDecimal;
import java.time.Instant;
/**
* An asynchronous result callback from MTN MoMo, stored whole alongside the fields
* worth querying on, and tied back to the call that caused it. Unlike
* PAYMENT_CALLBACKS this keeps every callback, duplicates included — an audit trail
* that silently drops repeats is not one.
*/
@Entity
@Getter
@Setter
@ToString(exclude = "Request")
public class MtnCallbackResponse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
/** Null when the callback could not be matched to an outbound request. */
@ManyToOne(targetEntity = MtnRequest.class, fetch = FetchType.LAZY)
@JoinColumn(name = "Request")
private MtnRequest Request;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentProviderType Provider;
@Column(length = 100)
private String ProviderReference;
@Column(length = 30)
private String ResultCode;
@Column(length = 255)
private String ResultDesc;
@Column(length = 50)
private String ReceiptNumber;
@Column(precision = 10, scale = 2)
private BigDecimal Amount;
@Column(length = 15)
private String PhoneNumber;
@Column(length = 20)
private String TransactionDate;
/** The callback exactly as it arrived. */
@Column(columnDefinition = "TEXT")
private String RawPayload;
/** False when no matching request could be found — an orphan worth alerting on. */
@Column(nullable = false)
private boolean Matched;
@Column(nullable = false)
private Instant CreatedAt;
}

View File

@@ -0,0 +1,68 @@
package com.test.payment.models.audit;
import com.test.payment.models.PaymentProviderType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Instant;
/**
* One outbound call to MTN MoMo, recorded verbatim: what we sent, where, and on whose
* behalf. Written asynchronously by ProviderCallAudit — nothing on the payment path
* waits for this row.
*/
@Entity
@Getter
@Setter
@ToString
public class MtnRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
/** Market-qualified provider, e.g. MTN_UG. */
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentProviderType Provider;
/** Which call this was: REQUEST_TO_PAY, STATUS_QUERY, TOKEN. */
@Column(nullable = false, length = 40)
private String Operation;
/**
* The payment attempt this call belongs to; null for calls with no payment
* (token fetches). A string, not the numeric id, so it can carry a NanoID.
*/
@Column(length = 36)
private String InitiationId;
/**
* The provider's own reference once known, so a later callback can be tied back
* to the exact call that produced it.
*/
@Column(length = 100)
private String ProviderReference;
@Column(length = 10)
private String HttpMethod;
@Column(length = 512)
private String Url;
/** Outbound body as sent, JSON. Credentials are redacted before this is stored. */
@Column(columnDefinition = "TEXT")
private String RequestBody;
@Column(nullable = false)
private Instant CreatedAt;
}

View File

@@ -0,0 +1,52 @@
package com.test.payment.models.audit;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Instant;
/**
* What MTN MoMo answered a MtnRequest with — including the failures, which is
* usually what an audit is actually needed for.
*/
@Entity
@Getter
@Setter
@ToString(exclude = "Request")
public class MtnResponse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@ManyToOne(targetEntity = MtnRequest.class, fetch = FetchType.LAZY)
@JoinColumn(name = "Request", nullable = false)
private MtnRequest Request;
/** HTTP status, or null when the call never got an answer (timeout, DNS, reset). */
private Integer HttpStatus;
/** Response body verbatim, JSON. */
@Column(columnDefinition = "TEXT")
private String ResponseBody;
/** Exception summary when the call failed outright. */
@Column(length = 512)
private String Error;
/** Round-trip time, for spotting MTN MoMo degrading before it starts erroring. */
private Long DurationMs;
@Column(nullable = false)
private Instant CreatedAt;
}