Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.payments.infrastructure.persistence

import com.fincore.events.OutboxStatus
import com.fincore.payments.domain.enum.PaymentStatus
import com.fincore.test.containers.PostgresContainerExtension
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.Instant
import java.util.UUID

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@ExtendWith(PostgresContainerExtension::class)
class PaymentsSchemaPersistenceIT(
@Autowired private val payments: PaymentRepository,
@Autowired private val paymentEvents: PaymentEventRepository,
@Autowired private val webhooks: ProcessedWebhookRepository,
@Autowired private val outbox: PaymentOutboxEventRepository,
) {
@AfterEach
fun cleanUp() {
paymentEvents.deleteAll()
webhooks.deleteAll()
outbox.deleteAll()
payments.deleteAll()
}

@Test
fun `should round-trip a payment preserving amount precision status and version`() {
val id = UUID.randomUUID()
val amount = BigDecimal("100.123456789012345678")
payments.saveAndFlush(
PaymentEntity(id, "order-1", amount, "USD", PaymentStatus.INITIATED, Instant.now(), 0L),
)

val reloaded = payments.findById(id).orElseThrow()
reloaded.reference shouldBe "order-1"
reloaded.amount.compareTo(amount) shouldBe 0
reloaded.currency.trim() shouldBe "USD"
reloaded.status shouldBe PaymentStatus.INITIATED
reloaded.version shouldBe 0L
}

@Test
fun `should round-trip a payment event with a json payload`() {
val id = UUID.randomUUID()
paymentEvents.saveAndFlush(
PaymentEventEntity(id, UUID.randomUUID(), "com.fincore.payment.initiated.v1", "{\"k\":\"v\"}", Instant.now()),
)

paymentEvents.findById(id).orElseThrow().eventType shouldBe "com.fincore.payment.initiated.v1"
}

@Test
fun `should round-trip an outbox row with pending status and no lease`() {
val id = UUID.randomUUID()
outbox.saveAndFlush(
PaymentOutboxEventEntity(
id,
"Payment",
"pay_1",
"com.fincore.payment.initiated.v1",
"{}",
OutboxStatus.PENDING,
Instant.now(),
null,
0,
null,
null,
),
)

val reloaded = outbox.findById(id).orElseThrow()
reloaded.status shouldBe OutboxStatus.PENDING
reloaded.attempts shouldBe 0
reloaded.leasedAt.shouldBeNull()
}

@Test
fun `should keep a single row per delivery id`() {
webhooks.saveAndFlush(ProcessedWebhookEntity("delivery-1", Instant.now()))
webhooks.saveAndFlush(ProcessedWebhookEntity("delivery-1", Instant.now()))

webhooks.count() shouldBe 1L
}

companion object {
@JvmStatic
@DynamicPropertySource
fun datasourceProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url") { PostgresContainerExtension.jdbcUrl }
registry.add("spring.datasource.username") { PostgresContainerExtension.username }
registry.add("spring.datasource.password") { PostgresContainerExtension.password }
registry.add("spring.jpa.hibernate.ddl-auto") { "none" }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.payments.infrastructure.persistence

import com.fincore.payments.domain.enum.PaymentStatus
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Id
import jakarta.persistence.Table
import jakarta.persistence.Version
import java.math.BigDecimal
import java.time.Instant
import java.util.UUID

@Entity
@Table(name = "payments", schema = "payments")
@Suppress("LongParameterList")
class PaymentEntity(
@Id
@Column(name = "id", nullable = false, updatable = false)
var id: UUID,
@Column(name = "reference", nullable = false, updatable = false)
var reference: String,
@Column(name = "amount", nullable = false, updatable = false)
var amount: BigDecimal,
@Column(name = "currency", nullable = false, updatable = false)
var currency: String,
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
var status: PaymentStatus,
@Column(name = "created_at", nullable = false, updatable = false)
var createdAt: Instant,
@Version
@Column(name = "version", nullable = false)
var version: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.payments.infrastructure.persistence

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.hibernate.annotations.Immutable
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.type.SqlTypes
import java.time.Instant
import java.util.UUID

@Entity
@Immutable
@Table(name = "payment_events", schema = "payments")
class PaymentEventEntity(
@Id
@Column(name = "id", nullable = false, updatable = false)
var id: UUID,
@Column(name = "payment_id", nullable = false, updatable = false)
var paymentId: UUID,
@Column(name = "event_type", nullable = false, updatable = false)
var eventType: String,
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "payload", nullable = false, updatable = false)
var payload: String,
@Column(name = "created_at", nullable = false, updatable = false)
var createdAt: Instant,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.payments.infrastructure.persistence

import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID

interface PaymentEventRepository : JpaRepository<PaymentEventEntity, UUID>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.payments.infrastructure.persistence

import com.fincore.events.OutboxStatus
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.type.SqlTypes
import java.time.Instant
import java.util.UUID

@Entity
@Table(name = "outbox_events", schema = "payments")
@Suppress("LongParameterList")
class PaymentOutboxEventEntity(
@Id
@Column(name = "id", nullable = false, updatable = false)
var id: UUID,
@Column(name = "aggregate_type", nullable = false, updatable = false)
var aggregateType: String,
@Column(name = "aggregate_id", nullable = false, updatable = false)
var aggregateId: String,
@Column(name = "event_type", nullable = false, updatable = false)
var eventType: String,
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "payload", nullable = false, updatable = false)
var payload: String,
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
var status: OutboxStatus,
@Column(name = "created_at", nullable = false, updatable = false)
var createdAt: Instant,
@Column(name = "published_at")
var publishedAt: Instant?,
@Column(name = "attempts", nullable = false)
var attempts: Int,
@Column(name = "last_error")
var lastError: String?,
@Column(name = "leased_at")
var leasedAt: Instant? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.payments.infrastructure.persistence

import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID

interface PaymentOutboxEventRepository : JpaRepository<PaymentOutboxEventEntity, UUID>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.payments.infrastructure.persistence

import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID

interface PaymentRepository : JpaRepository<PaymentEntity, UUID>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.payments.infrastructure.persistence

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.hibernate.annotations.Immutable
import java.time.Instant

@Entity
@Immutable
@Table(name = "processed_webhooks", schema = "payments")
class ProcessedWebhookEntity(
@Id
@Column(name = "delivery_id", nullable = false, updatable = false)
var deliveryId: String,
@Column(name = "received_at", nullable = false, updatable = false)
var receivedAt: Instant,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.payments.infrastructure.persistence

import org.springframework.data.jpa.repository.JpaRepository

interface ProcessedWebhookRepository : JpaRepository<ProcessedWebhookEntity, String>
5 changes: 5 additions & 0 deletions services/payments/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
spring:
application:
name: payments
jpa:
hibernate:
ddl-auto: none
liquibase:
change-log: classpath:db/changelog/db.changelog-master.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: BUSL-1.1
# SPDX-FileCopyrightText: 2026 FinCore Engine Authors
databaseChangeLog:
- include:
file: v0.1/001-schema.sql
relativeToChangelogFile: true
- include:
file: v0.1/010-payments.sql
relativeToChangelogFile: true
- include:
file: v0.1/011-payment-events.sql
relativeToChangelogFile: true
- include:
file: v0.1/012-processed-webhooks.sql
relativeToChangelogFile: true
- include:
file: v0.1/013-outbox-events.sql
relativeToChangelogFile: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
--liquibase formatted sql
-- SPDX-License-Identifier: BUSL-1.1
-- SPDX-FileCopyrightText: 2026 FinCore Engine Authors

--changeset fincore:payments-001-schema dbms:postgresql
CREATE SCHEMA IF NOT EXISTS payments;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--liquibase formatted sql
-- SPDX-License-Identifier: BUSL-1.1
-- SPDX-FileCopyrightText: 2026 FinCore Engine Authors

--changeset fincore:payments-010-payments dbms:postgresql
CREATE TABLE IF NOT EXISTS payments.payments (
id UUID NOT NULL DEFAULT gen_random_uuid(),
reference VARCHAR(140) NOT NULL,
amount NUMERIC(38, 18) NOT NULL,
currency CHAR(3) NOT NULL,
status VARCHAR(16) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
version BIGINT NOT NULL DEFAULT 0,
CONSTRAINT pk_payments PRIMARY KEY (id),
CONSTRAINT ck_payments_currency CHECK (currency ~ '^[A-Z]{3}$'),
CONSTRAINT ck_payments_status
CHECK (status IN ('INITIATED', 'SCREENING', 'SUBMITTED', 'SETTLED', 'FAILED', 'CANCELLED'))
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--liquibase formatted sql
-- SPDX-License-Identifier: BUSL-1.1
-- SPDX-FileCopyrightText: 2026 FinCore Engine Authors

--changeset fincore:payments-011-payment-events dbms:postgresql
CREATE TABLE IF NOT EXISTS payments.payment_events (
id UUID NOT NULL DEFAULT gen_random_uuid(),
payment_id UUID NOT NULL,
event_type VARCHAR(128) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT pk_payment_events PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS idx_payment_events_payment
ON payments.payment_events(payment_id, created_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--liquibase formatted sql
-- SPDX-License-Identifier: BUSL-1.1
-- SPDX-FileCopyrightText: 2026 FinCore Engine Authors

--changeset fincore:payments-012-processed-webhooks dbms:postgresql
CREATE TABLE IF NOT EXISTS payments.processed_webhooks (
delivery_id VARCHAR(200) NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT pk_processed_webhooks PRIMARY KEY (delivery_id)
);
Loading
Loading