Built with

There are some libraries that simplify integration of the payment systems, the most popular for PHP is OmniPay. We use it as a primary Payment Services integration library now.

ER diagram

plantuml

@startuml

legend right
        |<#FFDDDD>  <b>E</b>  |  <b>Entity</b> |
        |<#DDFFDD>  <b>V</b>  |  <b>Value Object</b>  |
        |<#FFFFDD>  <b>A</b>  |  <b>Auxiliary</b> |
endlegend

!define Entity(name,desc) class name as "desc" << (E,#EEEEEE) >> #FFDDDD
!define ValueObject(name,desc) class name as "desc" << (V,#FFFFFF) >> #DDFFDD
!define Auxiliary(name,desc) class name as "desc" << (A,#FFFFFF) >> #FFFFDD
!define primary_key(x) x TEXT
!define foreign_key(x, y) <i>x</i> --> <b>y</b>
!define value_object(x, y) x <b>y</b>
hide methods
hide stereotypes

package "Payments configuration" {
    Entity(merchant, "Merchant") {
        primary_key(id)
        email TEXT
    }

    Entity(gateway, "Gateway") {
        primary_key(id)
        foreign_key(merchant, Merchant)
        foreign_key(routes, Route[])
    }

    Entity(route, "Route") {
        primary_key(id)
        foreign_key(gateway, Gateway)
        foreign_key(paymentService, PaymentService)
        foreign_key(providerAccount, ProviderAccount)
    }

    Entity(providerAccount, ProviderAccount) {
        primary_key(id)
        foreign_key(credentials, Credentials)
        foreign_key(paymentProvider, PaymentProvider)
    }

    Entity(credentials, Credentials) {
        hashmap<string, string>
    }

    Entity(paymentPage, PaymentPage) {
        primary_key(id)
        foreign_key(gateway, Gateway)
        uiOptions hashmap<string, mixed>
        secretKey string
    }
}

package "Reference information" {
    Entity(paymentProvider, PaymentProvider) {
        primary_key(code)
        name TEXT
    }

    Entity(paymentService, PaymentService) {
        primary_key(code)
        foreign_key(currency, Currency)
        foreign_key(paymentFlow, PaymentFlow)
        foreign_key(paymentMethod, PaymentMethod)
    }

    ValueObject(currency, Currency) {
        primary_key(code)
    }

    ValueObject(paymentFlow, PaymentFlow) {
        flow ENUM[hpp, token]
    }

    ValueObject(paymentMethod, PaymentMethod) {
        primary_key(id)
        category TEXT
        name TEXT
    }
}

package "ValueObjects" {
    ValueObject(money, Money) {
        amount TEXT
        foreign_key(currency, Currency)
    }
}

package "Customers" {
    Entity(customer, Customer) {
       primary_key(id)
    }
}

package "Transaction" {
    Entity(paymentRequest, PaymentRequest) {
        primary_key(id)
        foreign_key(amount, Money)
        foreign_key(paymentPage, PaymentPage)
        state ENUM[paid, new]
        creationDate DateTimeImmutable
        foreign_key(customer, Customer)
        callbackUrl TEXT
        successUrl TEXT
        errorUrl TEXT
    }

    Entity(paymentTransaction, PaymentTransaction) {
        primary_key(id)
        foreign_key(paymentRequest, PaymentRequest)
        foreign_key(route, Route)
        foreign_key(amount, Money)
        foreign_key(commission, Money)
        createdAt DateTimeImmutable
        paidAt DateTimeImmutable
        state ENUM[new, paid, failed]
        foreign_key(confirmation, Confirmation)
    }

    Entity(paymentTransactionConfirmation, PaymentTransactionConfirmation) {
        primary_key(id)
        remoteId TEXT
        time DateTimeImmutable
        payer TEXT
    }

    Entity(paymentTransactionConfirmationNotification, PaymentTransactionConfirmationNotification) {
        primary_key(id)
        foreign_key(transaction, PaymentTransaction)
        foreign_key(confirmation, PaymentTransactionConfirmation)
        sentAt DateTimeImmutable
        accepted Boolean
        request TEXT
        response TEXT
    }

    paymentRequest -> money
    paymentRequest -down-> paymentPage
    paymentRequest -up-> customer
    paymentTransaction -> paymentRequest
    paymentTransaction -down-> route
    paymentTransaction -left-> paymentTransactionConfirmation
    paymentTransactionConfirmationNotification -down-> paymentTransaction
    paymentTransactionConfirmationNotification -down-> paymentTransactionConfirmation
}


gateway -> merchant
route -> gateway
paymentService -up-> route
paymentFlow -> paymentService
currency -up-> paymentService
paymentMethod -up-> paymentService
providerAccount <- route
providerAccount -> paymentProvider
providerAccount -left-> credentials
gateway -> paymentPage

@enduml

Workflow

planuml

@startuml
actor Customer

== Initialization ==
Customer->POS: Requests payment
activate POS
POS->"Merchant API": Creates PaymentRequest
activate "Merchant API"
"Merchant API"->POS: URL with a PaymentRequest ID
destroy "Merchant API"
POS->Customer: Redirect URL
deactivate POS
Customer->Merchant: User is being redirected to Merchant UI

== Choosing a payment provider ==
Merchant->Customer: Returns UI with different payment options
Customer->"Merchant API": Starts Transaction with the specific PaymentOption
activate Customer
activate "Merchant API"
"Merchant API"->"Service Provider": Initiate a payment request
activate "Service Provider"
"Service Provider"->"Merchant API": Returns a redirect link
deactivate "Service Provider"
"Merchant API"->Customer: Returns a redirect link
deactivate "Merchant API"
deactivate Customer

== Payment handling ==
Customer->"Service Provider": Follows a redirect
"Service Provider"->"Service Provider": Handles Customer's Payment Method
"Service Provider"->"Merchant": Redirects Customer back to Merchant
"Merchant"->Customer: Provides a "Waiting for confirmation UI"
"Service Provider"->"Merchant API": Sends a payment confirmation
"Merchant API"-->>"Service Provider": Confirms payment acceptance
"Merchant API"->POS: Propagates a payment notification
Customer->"Merchant API": Customer periodically asks for the payment confirmation
Customer->POS: Once confirmed, gets redirected to the POS

@enduml