package com.test.payment.models; import com.test.payment.models.audit.AirtelRequest; import com.test.payment.models.audit.MpesaRequest; import com.test.payment.models.audit.MtnRequest; 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.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import java.math.BigDecimal; import java.time.LocalDateTime; @Entity @Getter @Setter @Builder @AllArgsConstructor @NoArgsConstructor @ToString(exclude = {"Initiation", "MpesaRequest", "AirtelRequest", "MtnRequest", "MpesaCallback", "AirtelCallback", "MtnCallback"}) public class Transaction { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long Id; /** * The attempt this consolidates. Unique: one transaction per initiation, * whichever path (callback, query, rejection, reconciliation) resolved it. */ @ManyToOne(targetEntity = PaymentInitiation.class, fetch = FetchType.LAZY) @JoinColumn(name = "Initiation", nullable = false, unique = true) private PaymentInitiation Initiation; @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) private PaymentProviderType Provider; /** * The outbound call that started this payment, and through it everything the * operator returned. Exactly one of the three is ever set — whichever operator * owns the row — because the request tables are per operator and share no * supertype. */ @ManyToOne(targetEntity = MpesaRequest.class, fetch = FetchType.LAZY) @JoinColumn(name = "MpesaRequest") private MpesaRequest MpesaRequest; @ManyToOne(targetEntity = AirtelRequest.class, fetch = FetchType.LAZY) @JoinColumn(name = "AirtelRequest") private AirtelRequest AirtelRequest; @ManyToOne(targetEntity = MtnRequest.class, fetch = FetchType.LAZY) @JoinColumn(name = "MtnRequest") private MtnRequest MtnRequest; /** The callback that resolved the payment, again one per operator. */ @ManyToOne(targetEntity = MpesaPaymentCallback.class, fetch = FetchType.LAZY) @JoinColumn(name = "MpesaCallback") private MpesaPaymentCallback MpesaCallback; @ManyToOne(targetEntity = AirtelPaymentCallback.class, fetch = FetchType.LAZY) @JoinColumn(name = "AirtelCallback") private AirtelPaymentCallback AirtelCallback; @ManyToOne(targetEntity = MtnPaymentCallback.class, fetch = FetchType.LAZY) @JoinColumn(name = "MtnCallback") private MtnPaymentCallback MtnCallback; @Column(length = 100) private String ProviderReference; @Column(length = 100) private String SecondaryReference; @Column(length = 15) private String PhoneNumber; @Column(precision = 10, scale = 2) private BigDecimal Amount; @Column(length = 50) private String AccountReference; @ManyToOne(targetEntity = Status.class, fetch = FetchType.LAZY) @JoinColumn(name = "Status", nullable = false) private Status Status; @Column(length = 30) private String ResultCode; @Column(length = 255) private String ResultDesc; @Column(length = 50) private String ReceiptNumber; @Column(length = 20) private String TransactionDate; /** What resolved the payment: CALLBACK, QUERY, REJECTION, ERROR, RECONCILIATION. Null while open. */ @Column(length = 20) private String ResolvedBy; @Column(nullable = false) private LocalDateTime CreatedAt; private LocalDateTime UpdatedAt; /** True once the outbound request has been recorded against this transaction. */ public boolean hasRequest() { return MpesaRequest != null || AirtelRequest != null || MtnRequest != null; } /** True once a result callback has been tied to this transaction. */ public boolean hasCallback() { return MpesaCallback != null || AirtelCallback != null || MtnCallback != null; } /** The status name ("Pending", "Paid", ...), or null when unset. */ public String statusName() { return Status == null ? null : Status.getName(); } /** Convenience for logging and DTOs that only need the initiation's id. */ public Long initiationId() { return Initiation == null ? null : Initiation.getId(); } }