diff --git a/README.md b/README.md
index 554985bdb..613e87e46 100644
--- a/README.md
+++ b/README.md
@@ -538,6 +538,13 @@ Base application is reduced to two REST resources:
Tests cover the supported functionality of `rest-data-panache`: CRUD operations, `json` and `hal+json` data types,
invalid input, filtering, sorting, pagination.
+### `sql-db/narayana-transactions`
+
+Verifies Quarkus transaction programmatic API.
+Base application contains REST resource `TransferResource` and three main services: `TransferTransactionService`, `TransferWithdrawalService`
+and `TransferTopUpService` which implement various bank transactions. The main scenario is implemented in `TransactionGeneralUsageIT`
+and checks whether transactions and rollbacks always done in full.
+
### `security/basic`
Verifies the simplest way of doing authn/authz.
diff --git a/pom.xml b/pom.xml
index 957381a04..e5f2b88b2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -557,6 +557,7 @@
sql-db/hibernate-reactive
sql-db/reactive-vanilla
sql-db/hibernate-fulltext-search
+ sql-db/narayana-transactions
diff --git a/sql-db/narayana-transactions/pom.xml b/sql-db/narayana-transactions/pom.xml
new file mode 100644
index 000000000..da2c1e748
--- /dev/null
+++ b/sql-db/narayana-transactions/pom.xml
@@ -0,0 +1,108 @@
+
+
+ 4.0.0
+
+ io.quarkus.ts.qe
+ parent
+ 1.0.0-SNAPSHOT
+ ../..
+
+ narayana-transactions
+ jar
+ Quarkus QE TS: SQL Database: Narayana-transactions
+
+
+ io.quarkus
+ quarkus-hibernate-orm-panache
+
+
+ io.quarkus
+ quarkus-resteasy-jackson
+
+
+ io.quarkus
+ quarkus-jdbc-mariadb
+
+
+ io.quarkus
+ quarkus-jdbc-mssql
+
+
+ io.quarkus
+ quarkus-jdbc-mysql
+
+
+ io.quarkus
+ quarkus-jdbc-postgresql
+
+
+ io.quarkus
+ quarkus-jdbc-oracle
+
+
+ io.quarkus
+ quarkus-smallrye-health
+
+
+
+ io.quarkus
+ quarkus-smallrye-openapi
+
+
+
+ io.quarkus
+ quarkus-opentelemetry
+
+
+
+ io.quarkus
+ quarkus-micrometer-registry-prometheus
+
+
+
+ io.quarkus.qe
+ quarkus-test-service-database
+ test
+
+
+ io.quarkus.qe
+ quarkus-test-service-jaeger
+ test
+
+
+
+
+ native
+
+
+ native
+
+
+
+
+
+ skip-tests-on-windows
+
+
+ windows
+
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+ maven-failsafe-plugin
+
+ true
+
+
+
+
+
+
+
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountEntity.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountEntity.java
new file mode 100644
index 000000000..f1368e2bc
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountEntity.java
@@ -0,0 +1,105 @@
+package io.quarkus.ts.transactions;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Objects;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+
+import io.quarkus.hibernate.orm.panache.PanacheEntity;
+import io.quarkus.panache.common.Parameters;
+import io.quarkus.panache.common.Sort;
+
+@Entity(name = "account")
+public class AccountEntity extends PanacheEntity {
+ @Column(nullable = false)
+ private String name;
+ @Column(nullable = false)
+ private String lastName;
+ @Column(unique = true, nullable = false)
+ private String accountNumber;
+ @Column(precision = 10, scale = 2, nullable = false)
+ private int amount;
+ private Timestamp updatedAt;
+ @Column(nullable = false)
+ private Timestamp createdAt;
+
+ public static boolean exist(String accountNumber) {
+ return Objects.nonNull(findAccount(accountNumber));
+ }
+
+ public static AccountEntity findAccount(String accountNumber) {
+ return find("accountNumber", accountNumber).firstResult();
+ }
+
+ public static int updateAmount(String accountNumber, int amount) {
+ Timestamp currentTime = new Timestamp(System.currentTimeMillis());
+ int updatedRecordsAmount = update("amount = :amount, updatedAt = :updatedAt where accountNumber = :account",
+ Parameters.with("amount", amount)
+ .and("updatedAt", currentTime)
+ .and("account", accountNumber));
+ flush();
+ return updatedRecordsAmount;
+ }
+
+ public static List getAllAccountsRecords() {
+ return findAll(Sort.by("createdAt").descending()).list();
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public int getAmount() {
+ return amount;
+ }
+
+ public void setAmount(int amount) {
+ this.amount = amount;
+ }
+
+ public Timestamp getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setUpdatedAt(Timestamp updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+
+ public Timestamp getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Timestamp createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ public void setAccountNumber(String accountNumber) {
+ this.accountNumber = accountNumber;
+ }
+}
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountService.java
new file mode 100644
index 000000000..bd9aa13e2
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountService.java
@@ -0,0 +1,54 @@
+package io.quarkus.ts.transactions;
+
+import static io.quarkus.ts.transactions.AccountEntity.exist;
+
+import java.util.List;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.NotFoundException;
+
+import org.jboss.logging.Logger;
+
+@ApplicationScoped
+public class AccountService {
+
+ private static final Logger LOG = Logger.getLogger(AccountService.class);
+
+ public boolean isPresent(String accountNumber) {
+ if (!exist(accountNumber)) {
+ String msg = String.format("Account %s doesn't exist", accountNumber);
+ LOG.warn(msg);
+ throw new NotFoundException(msg);
+ }
+
+ return true;
+ }
+
+ public int increaseBalance(String account, int amount) {
+ AccountEntity accountEntity = AccountEntity.findAccount(account);
+ int updatedAmount = accountEntity.getAmount() + amount;
+ AccountEntity.updateAmount(account, updatedAmount);
+ return AccountEntity.findAccount(account).getAmount();
+ }
+
+ public int decreaseBalance(String account, int amount) {
+ AccountEntity accountEntity = AccountEntity.findAccount(account);
+ int updatedAmount = accountEntity.getAmount() - amount;
+ if (updatedAmount < 0) {
+ String msg = String.format("Account %s Not enough balance.", account);
+ LOG.warn(msg);
+ throw new BadRequestException(msg);
+ }
+ AccountEntity.updateAmount(account, updatedAmount);
+ return updatedAmount;
+ }
+
+ public List getAllAccounts() {
+ return AccountEntity.getAllAccountsRecords();
+ }
+
+ public AccountEntity getAccount(String accountNumber) {
+ return AccountEntity.findAccount(accountNumber);
+ }
+}
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalEntity.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalEntity.java
new file mode 100644
index 000000000..1fdd295dd
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalEntity.java
@@ -0,0 +1,96 @@
+package io.quarkus.ts.transactions;
+
+import java.sql.Timestamp;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+
+import io.quarkus.hibernate.orm.panache.PanacheEntity;
+import io.quarkus.panache.common.Parameters;
+import io.quarkus.panache.common.Sort;
+
+@Entity(name = "journal")
+public class JournalEntity extends PanacheEntity {
+
+ @Column(nullable = false)
+ private String annotation;
+ @Column(nullable = false)
+ private String accountTo;
+ @Column(nullable = false)
+ private String accountFrom;
+ @Column(nullable = false)
+ private int amount;
+ @Column(nullable = false)
+ private Timestamp createdAt;
+
+ public JournalEntity() {
+ }
+
+ public JournalEntity(String accountFrom, String accountTo, String annotation, int amount) {
+ this.accountFrom = accountFrom;
+ this.accountTo = accountTo;
+ this.annotation = annotation;
+ this.amount = amount;
+ this.createdAt = new Timestamp(System.currentTimeMillis());
+ }
+
+ public JournalEntity addLog() {
+ persistAndFlush();
+ return this;
+ }
+
+ public static JournalEntity getLatestJournalRecord(String accountNumber) {
+ return find("accountFrom = :accountFrom",
+ Sort.by("createdAt").descending(),
+ Parameters.with("accountFrom", accountNumber))
+ .firstResult();
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getAnnotation() {
+ return annotation;
+ }
+
+ public void setAnnotation(String annotation) {
+ this.annotation = annotation;
+ }
+
+ public String getAccountTo() {
+ return accountTo;
+ }
+
+ public void setAccountTo(String accountTo) {
+ this.accountTo = accountTo;
+ }
+
+ public String getAccountFrom() {
+ return accountFrom;
+ }
+
+ public void setAccountFrom(String accountFrom) {
+ this.accountFrom = accountFrom;
+ }
+
+ public int getAmount() {
+ return amount;
+ }
+
+ public void setAmount(int amount) {
+ this.amount = amount;
+ }
+
+ public Timestamp getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Timestamp createdAt) {
+ this.createdAt = createdAt;
+ }
+}
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalService.java
new file mode 100644
index 000000000..85435720e
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalService.java
@@ -0,0 +1,18 @@
+package io.quarkus.ts.transactions;
+
+import static io.quarkus.ts.transactions.JournalEntity.getLatestJournalRecord;
+
+import javax.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class JournalService {
+
+ public JournalEntity addToJournal(String accountFrom, String accountTo, String annotation, int amount) {
+ JournalEntity journal = new JournalEntity(accountFrom, accountTo, annotation, amount);
+ return journal.addLog();
+ }
+
+ public JournalEntity getLatestJournalRecordByAccountNumber(String accountNumber) {
+ return getLatestJournalRecord(accountNumber);
+ }
+}
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferDTO.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferDTO.java
new file mode 100644
index 000000000..23f721906
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferDTO.java
@@ -0,0 +1,33 @@
+package io.quarkus.ts.transactions;
+
+import java.io.Serializable;
+
+public class TransferDTO implements Serializable {
+ private String accountTo;
+ private String accountFrom;
+ private int amount;
+
+ public String getAccountTo() {
+ return accountTo;
+ }
+
+ public void setAccountTo(String accountTo) {
+ this.accountTo = accountTo;
+ }
+
+ public String getAccountFrom() {
+ return accountFrom;
+ }
+
+ public void setAccountFrom(String accountFrom) {
+ this.accountFrom = accountFrom;
+ }
+
+ public int getAmount() {
+ return amount;
+ }
+
+ public void setAmount(int amount) {
+ this.amount = amount;
+ }
+}
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferProcessor.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferProcessor.java
new file mode 100644
index 000000000..ccb98445c
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferProcessor.java
@@ -0,0 +1,20 @@
+package io.quarkus.ts.transactions;
+
+import javax.inject.Inject;
+
+public abstract class TransferProcessor {
+
+ @Inject
+ AccountService accountService;
+
+ @Inject
+ JournalService journalService;
+
+ protected void verifyAccounts(String... accounts) {
+ for (String account : accounts) {
+ accountService.isPresent(account);
+ }
+ }
+
+ public abstract JournalEntity makeTransaction(String from, String to, int amount);
+}
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferResource.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferResource.java
new file mode 100644
index 000000000..589585a8e
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferResource.java
@@ -0,0 +1,98 @@
+package io.quarkus.ts.transactions;
+
+import static javax.ws.rs.core.Response.Status.CREATED;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+@Path("/transfer")
+public class TransferResource {
+
+ @Inject
+ @Named("TransferTransactionService")
+ TransferProcessor regularTransaction;
+
+ @Inject
+ @Named("TransferTopUpService")
+ TransferProcessor topUp;
+
+ @Inject
+ @Named("TransferWithdrawalService")
+ TransferProcessor withdrawal;
+
+ @Inject
+ JournalService journalService;
+
+ @Inject
+ AccountService accountService;
+
+ /**
+ * Transaction represent a transfer funds from one account to another account
+ * On the journal will look like a transaction with different from / to accounts and the annotation transaction.
+ */
+ @Path("/transaction")
+ @POST
+ public Response makeRegularTransaction(TransferDTO transferDTO) {
+ Long ID = regularTransaction.makeTransaction(transferDTO.getAccountFrom(), transferDTO.getAccountTo(),
+ transferDTO.getAmount()).getId();
+
+ return Response.ok(ID).status(CREATED.getStatusCode()).build();
+ }
+
+ /**
+ * TopUp represent a transfer funds transaction to your account, but the money doesn't come from another account.
+ * On the journal will look like a transaction with the same from / to account and the annotation top-up.
+ */
+ @Path("/top-up")
+ @POST
+ public Response topup(TransferDTO transferDTO) {
+ Long ID = topUp.makeTransaction(transferDTO.getAccountFrom(), transferDTO.getAccountTo(),
+ transferDTO.getAmount()).getId();
+
+ return Response.ok(ID).status(CREATED.getStatusCode()).build();
+ }
+
+ /**
+ * Withdrawal represent a take off funds from your account, but you don't transfer this money to another account
+ * On the journal will look like a transaction with the same from / to accounts and the annotation withdrawal.
+ */
+ @Path("/withdrawal")
+ @POST
+ public Response makeMoneyTransaction(TransferDTO transferDTO) {
+ Long ID = withdrawal.makeTransaction(transferDTO.getAccountFrom(), transferDTO.getAccountTo(),
+ transferDTO.getAmount()).getId();
+
+ return Response.ok(ID).status(CREATED.getStatusCode()).build();
+ }
+
+ @Path("/accounts/")
+ @GET
+ public List getAccounts() {
+ return accountService.getAllAccounts();
+ }
+
+ @Path("/accounts/{account_id}")
+ @GET
+ public AccountEntity getAccountById(@PathParam("account_id") String accountNumber) {
+ return accountService.getAccount(accountNumber);
+ }
+
+ @Path("/journal/latest/{account_id}")
+ @GET
+ public JournalEntity getLatestJournalRecord(@PathParam("account_id") String accountNumber) {
+ return journalService.getLatestJournalRecordByAccountNumber(accountNumber);
+ }
+
+}
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTopUpService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTopUpService.java
new file mode 100644
index 000000000..0a2f6be5e
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTopUpService.java
@@ -0,0 +1,48 @@
+package io.quarkus.ts.transactions;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Named;
+
+import org.jboss.logging.Logger;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.quarkus.narayana.jta.QuarkusTransaction;
+import io.quarkus.narayana.jta.RunOptions;
+
+@ApplicationScoped
+@Named("TransferTopUpService")
+public class TransferTopUpService extends TransferProcessor {
+ private static final Logger LOG = Logger.getLogger(TransferTopUpService.class);
+ private final static String ANNOTATION_TOP_UP = "user top up";
+ private final static int TRANSACTION_TIMEOUT_SEC = 10;
+ private final MeterRegistry registry;
+ private long transactionsAmount;
+
+ public TransferTopUpService(MeterRegistry registry) {
+ this.registry = registry;
+ registry.gauge("transaction.topup.amount", this, TransferTopUpService::getTransactionsAmount);
+ }
+
+ public JournalEntity makeTransaction(String from, String to, int amount) {
+ LOG.infof("TopUp account %s amount %s", from, amount);
+ verifyAccounts(to);
+ JournalEntity journal = QuarkusTransaction.call(QuarkusTransaction.runOptions()
+ .timeout(TRANSACTION_TIMEOUT_SEC)
+ .exceptionHandler(t -> {
+ transactionsAmount--;
+ return RunOptions.ExceptionResult.ROLLBACK;
+ })
+ .semantic(RunOptions.Semantic.REQUIRE_NEW), () -> {
+ transactionsAmount++;
+ JournalEntity journalentity = journalService.addToJournal(from, to, ANNOTATION_TOP_UP, amount);
+ accountService.increaseBalance(from, amount);
+ return journalentity;
+ });
+ LOG.infof("TopUp completed account %s", from);
+ return journal;
+ }
+
+ public long getTransactionsAmount() {
+ return transactionsAmount;
+ }
+}
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTransactionService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTransactionService.java
new file mode 100644
index 000000000..25bcc0110
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTransactionService.java
@@ -0,0 +1,52 @@
+package io.quarkus.ts.transactions;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Named;
+
+import org.jboss.logging.Logger;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.quarkus.narayana.jta.QuarkusTransaction;
+
+@ApplicationScoped
+@Named("TransferTransactionService")
+public class TransferTransactionService extends TransferProcessor {
+
+ private static final Logger LOG = Logger.getLogger(TransferTransactionService.class);
+ private final static String ANNOTATION_TRANSACTION = "user transaction to other user";
+ private final static int TRANSACTION_TIMEOUT_SEC = 10;
+ private final MeterRegistry registry;
+ private long transactionsAmount;
+
+ public TransferTransactionService(MeterRegistry registry) {
+ this.registry = registry;
+ registry.gauge("transaction.regular.amount", this, TransferTransactionService::getTransactionsAmount);
+ }
+
+ public JournalEntity makeTransaction(String from, String to, int amount) {
+ JournalEntity journal = null;
+ LOG.infof("Regular transaction, from %s to %s amount %s", from, to, amount);
+ try {
+ // please don't move this gauge after commit statement, because we want to test the gauges after a rollback
+ transactionsAmount++;
+ verifyAccounts(from, to);
+ QuarkusTransaction.begin(QuarkusTransaction.beginOptions().timeout(TRANSACTION_TIMEOUT_SEC));
+ journal = journalService.addToJournal(from, to, ANNOTATION_TRANSACTION, amount);
+ accountService.decreaseBalance(from, amount);
+ accountService.increaseBalance(to, amount);
+ QuarkusTransaction.commit();
+ LOG.infof("Regular transaction completed, from %s to %s", from, to);
+ } catch (Exception e) {
+ LOG.errorf("Error on regular transaction %s ", e.getMessage());
+ QuarkusTransaction.rollback();
+ transactionsAmount--;
+ throw e;
+ }
+
+ return journal;
+ }
+
+ public long getTransactionsAmount() {
+ return transactionsAmount;
+ }
+}
diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferWithdrawalService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferWithdrawalService.java
new file mode 100644
index 000000000..f3332a312
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferWithdrawalService.java
@@ -0,0 +1,51 @@
+package io.quarkus.ts.transactions;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Named;
+
+import org.jboss.logging.Logger;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.quarkus.narayana.jta.QuarkusTransaction;
+
+@ApplicationScoped
+@Named("TransferWithdrawalService")
+public class TransferWithdrawalService extends TransferProcessor {
+
+ private static final Logger LOG = Logger.getLogger(TransferWithdrawalService.class);
+ private final static int TRANSACTION_TIMEOUT_SEC = 10;
+ private final static String ANNOTATION_WITHDRAWAL = "user withdrawal";
+ private final MeterRegistry registry;
+ private long transactionsAmount;
+
+ public TransferWithdrawalService(MeterRegistry registry) {
+ this.registry = registry;
+ registry.gauge("transaction.withdrawal.amount", this, TransferWithdrawalService::getTransactionsAmount);
+ }
+
+ public JournalEntity makeTransaction(String from, String to, int amount) {
+ JournalEntity journal = null;
+ try {
+ LOG.infof("Withdrawal account %s amount %s", from, amount);
+ // please don't move this gauge after commit statement, because we want to test the gauges after a rollback
+ transactionsAmount++;
+ verifyAccounts(from);
+ QuarkusTransaction.begin(QuarkusTransaction.beginOptions().timeout(TRANSACTION_TIMEOUT_SEC));
+ journal = journalService.addToJournal(from, to, ANNOTATION_WITHDRAWAL, amount);
+ accountService.decreaseBalance(from, amount);
+ QuarkusTransaction.commit();
+ LOG.infof("Withdrawal completed account %s", from);
+ } catch (Exception e) {
+ LOG.errorf("Error on withdrawal transaction %s ", e.getMessage());
+ QuarkusTransaction.rollback();
+ transactionsAmount--;
+ throw e;
+ }
+
+ return journal;
+ }
+
+ public long getTransactionsAmount() {
+ return transactionsAmount;
+ }
+}
diff --git a/sql-db/narayana-transactions/src/main/resources/application.properties b/sql-db/narayana-transactions/src/main/resources/application.properties
new file mode 100644
index 000000000..32005d62e
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/resources/application.properties
@@ -0,0 +1,6 @@
+quarkus.datasource.db-kind=postgresql
+quarkus.hibernate-orm.database.charset=utf-8
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.hibernate-orm.sql-load-script=import.sql
+quarkus.opentelemetry.enabled=false
+quarkus.application.name=narayanaTransactions
diff --git a/sql-db/narayana-transactions/src/main/resources/import.sql b/sql-db/narayana-transactions/src/main/resources/import.sql
new file mode 100644
index 000000000..b66c0920b
--- /dev/null
+++ b/sql-db/narayana-transactions/src/main/resources/import.sql
@@ -0,0 +1,6 @@
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Garcilaso', 'de la Vega', 'CZ9250512252717368964232', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Miguel', 'de Cervantes', 'SK0389852379529966291984', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Luis', 'de Góngora', 'ES8521006742088984966816', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Lope', 'de Vega', 'FR9317569000409377431694J37', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Francisco', 'Quevedo', 'ES8521006742088984966817', 100, null, CURRENT_TIMESTAMP);
+
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MariaDbTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MariaDbTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..e2632ace3
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MariaDbTransactionGeneralUsageIT.java
@@ -0,0 +1,28 @@
+package io.quarkus.ts.transactions;
+
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import io.quarkus.test.bootstrap.MariaDbService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@QuarkusScenario
+@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)")
+public class MariaDbTransactionGeneralUsageIT extends TransactionCommons {
+
+ static final int MARIADB_PORT = 3306;
+
+ @Container(image = "${mariadb.10.image}", port = MARIADB_PORT, expectedLog = "socket: '/run/mysqld/mysqld.sock' port: "
+ + MARIADB_PORT)
+ static MariaDbService database = new MariaDbService();
+
+ @QuarkusApplication
+ static RestService app = new RestService().withProperties("mariadb_app.properties")
+ .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl)
+ .withProperty("quarkus.datasource.username", database.getUser())
+ .withProperty("quarkus.datasource.password", database.getPassword())
+ .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl);
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MssqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MssqlTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..cfbf0a4b7
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MssqlTransactionGeneralUsageIT.java
@@ -0,0 +1,27 @@
+package io.quarkus.ts.transactions;
+
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.bootstrap.SqlServerService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@QuarkusScenario
+@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)")
+public class MssqlTransactionGeneralUsageIT extends TransactionCommons {
+
+ private static final int MSSQL_PORT = 1433;
+
+ @Container(image = "${mssql.image}", port = MSSQL_PORT, expectedLog = "Service Broker manager has started")
+ static SqlServerService database = new SqlServerService();
+
+ @QuarkusApplication
+ public static final RestService app = new RestService().withProperties("mssql.properties")
+ .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl)
+ .withProperty("quarkus.datasource.username", database.getUser())
+ .withProperty("quarkus.datasource.password", database.getPassword())
+ .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl);
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MysqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MysqlTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..14e0f4174
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MysqlTransactionGeneralUsageIT.java
@@ -0,0 +1,30 @@
+package io.quarkus.ts.transactions;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import io.quarkus.test.bootstrap.MySqlService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+// TODO https://github.com/quarkus-qe/quarkus-test-suite/issues/756
+@Tag("fips-incompatible") // native-mode
+@QuarkusScenario
+@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)")
+public class MysqlTransactionGeneralUsageIT extends TransactionCommons {
+
+ static final int MYSQL_PORT = 3306;
+
+ @Container(image = "${mysql.57.image}", port = MYSQL_PORT, expectedLog = "port: " + MYSQL_PORT)
+ static final MySqlService database = new MySqlService();
+
+ @QuarkusApplication
+ static RestService app = new RestService().withProperties("mysql.properties")
+ .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl)
+ .withProperty("quarkus.datasource.username", database.getUser())
+ .withProperty("quarkus.datasource.password", database.getPassword())
+ .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl);
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMariaDbTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMariaDbTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..fb30e84dc
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMariaDbTransactionGeneralUsageIT.java
@@ -0,0 +1,26 @@
+package io.quarkus.ts.transactions;
+
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+import io.quarkus.test.bootstrap.MariaDbService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.OpenShiftScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@OpenShiftScenario
+@EnabledIfSystemProperty(named = "ts.redhat.registry.enabled", matches = "true")
+public class OpenShiftMariaDbTransactionGeneralUsageIT extends TransactionCommons {
+
+ static final int MARIADB_PORT = 3306;
+
+ @Container(image = "${mariadb.105.image}", port = MARIADB_PORT, expectedLog = "Only MySQL server logs after this point")
+ static MariaDbService database = new MariaDbService();
+
+ @QuarkusApplication
+ static RestService app = new RestService().withProperties("mariadb_app.properties")
+ .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl)
+ .withProperty("quarkus.datasource.username", database.getUser())
+ .withProperty("quarkus.datasource.password", database.getPassword())
+ .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl);
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMsqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMsqlTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..f2590784b
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMsqlTransactionGeneralUsageIT.java
@@ -0,0 +1,25 @@
+package io.quarkus.ts.transactions;
+
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+import io.quarkus.test.bootstrap.MySqlService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.OpenShiftScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@OpenShiftScenario
+@EnabledIfSystemProperty(named = "ts.redhat.registry.enabled", matches = "true")
+public class OpenShiftMsqlTransactionGeneralUsageIT extends TransactionCommons {
+ static final int MYSQL_PORT = 3306;
+
+ @Container(image = "${mysql.80.image}", port = MYSQL_PORT, expectedLog = "Only MySQL server logs after this point")
+ static MySqlService database = new MySqlService();
+
+ @QuarkusApplication
+ static RestService app = new RestService().withProperties("mysql.properties")
+ .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl)
+ .withProperty("quarkus.datasource.username", database.getUser())
+ .withProperty("quarkus.datasource.password", database.getPassword())
+ .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl);
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMssqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMssqlTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..def63bff2
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMssqlTransactionGeneralUsageIT.java
@@ -0,0 +1,10 @@
+package io.quarkus.ts.transactions;
+
+import org.junit.jupiter.api.Disabled;
+
+import io.quarkus.test.scenarios.OpenShiftScenario;
+
+@OpenShiftScenario
+@Disabled("https://github.com/microsoft/mssql-docker/issues/769")
+public class OpenShiftMssqlTransactionGeneralUsageIT extends MssqlTransactionGeneralUsageIT {
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftOracleTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftOracleTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..2c4b89183
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftOracleTransactionGeneralUsageIT.java
@@ -0,0 +1,10 @@
+package io.quarkus.ts.transactions;
+
+import org.junit.jupiter.api.Disabled;
+
+import io.quarkus.test.scenarios.OpenShiftScenario;
+
+@OpenShiftScenario
+@Disabled("https://github.com/quarkus-qe/quarkus-test-suite/issues/246")
+public class OpenShiftOracleTransactionGeneralUsageIT extends OracleTransactionGeneralUsageIT {
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftPostgresqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftPostgresqlTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..5461f9200
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftPostgresqlTransactionGeneralUsageIT.java
@@ -0,0 +1,7 @@
+package io.quarkus.ts.transactions;
+
+import io.quarkus.test.scenarios.OpenShiftScenario;
+
+@OpenShiftScenario
+public class OpenShiftPostgresqlTransactionGeneralUsageIT extends PostgresqlTransactionGeneralUsageIT {
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OracleTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OracleTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..ede185809
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OracleTransactionGeneralUsageIT.java
@@ -0,0 +1,27 @@
+package io.quarkus.ts.transactions;
+
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import io.quarkus.test.bootstrap.OracleService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@QuarkusScenario
+@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)")
+public class OracleTransactionGeneralUsageIT extends TransactionCommons {
+
+ static final int ORACLE_PORT = 1521;
+
+ @Container(image = "${oracle.image}", port = ORACLE_PORT, expectedLog = "DATABASE IS READY TO USE!")
+ static OracleService database = new OracleService();
+
+ @QuarkusApplication
+ static RestService app = new RestService().withProperties("oracle.properties")
+ .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl)
+ .withProperty("quarkus.datasource.username", database.getUser())
+ .withProperty("quarkus.datasource.password", database.getPassword())
+ .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl);
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/PostgresqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/PostgresqlTransactionGeneralUsageIT.java
new file mode 100644
index 000000000..7a41f86fc
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/PostgresqlTransactionGeneralUsageIT.java
@@ -0,0 +1,28 @@
+package io.quarkus.ts.transactions;
+
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import io.quarkus.test.bootstrap.PostgresqlService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@QuarkusScenario
+@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)")
+public class PostgresqlTransactionGeneralUsageIT extends TransactionCommons {
+
+ static final int POSTGRESQL_PORT = 5432;
+
+ @Container(image = "${postgresql.latest.image}", port = POSTGRESQL_PORT, expectedLog = "listening on IPv4 address")
+ static final PostgresqlService database = new PostgresqlService().withProperty("PGDATA", "/tmp/psql");
+
+ @QuarkusApplication
+ public static final RestService app = new RestService()
+ .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl)
+ .withProperty("quarkus.opentelemetry.enabled", "true")
+ .withProperty("quarkus.datasource.username", database.getUser())
+ .withProperty("quarkus.datasource.password", database.getPassword())
+ .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl);
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/SwaggerUiIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/SwaggerUiIT.java
new file mode 100644
index 000000000..bacb53d14
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/SwaggerUiIT.java
@@ -0,0 +1,28 @@
+package io.quarkus.ts.transactions;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.containsString;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.scenarios.annotations.DisabledOnNative;
+import io.quarkus.test.services.DevModeQuarkusApplication;
+
+@QuarkusScenario
+@DisabledOnNative
+public class SwaggerUiIT {
+
+ @DevModeQuarkusApplication
+ static RestService app = new RestService();
+
+ @Test
+ public void smokeTestSwaggerUi() {
+ given()
+ .when().get("/q/swagger-ui")
+ .then()
+ .statusCode(200)
+ .body(containsString("/openapi"));
+ }
+}
diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/TransactionCommons.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/TransactionCommons.java
new file mode 100644
index 000000000..4cc66a33b
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/TransactionCommons.java
@@ -0,0 +1,208 @@
+package io.quarkus.ts.transactions;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.bootstrap.JaegerService;
+import io.quarkus.test.services.JaegerContainer;
+import io.restassured.http.ContentType;
+import io.restassured.response.Response;
+
+public abstract class TransactionCommons {
+
+ static final String ACCOUNT_NUMBER_MIGUEL = "SK0389852379529966291984";
+ static final String ACCOUNT_NUMBER_GARCILASO = "FR9317569000409377431694J37";
+ static final String ACCOUNT_NUMBER_LUIS = "ES8521006742088984966816";
+ static final String ACCOUNT_NUMBER_LOPE = "CZ9250512252717368964232";
+ static final String ACCOUNT_NUMBER_FRANCISCO = "ES8521006742088984966817";
+ static final int ASSERT_SERVICE_TIMEOUT_MINUTES = 1;
+ private Response jaegerResponse;
+
+ @JaegerContainer(useOtlpCollector = true, expectedLog = "\"Health Check state change\",\"status\":\"ready\"")
+ static final JaegerService jaeger = new JaegerService();
+
+ @Tag("QUARKUS-2492")
+ @Test
+ public void verifyNarayanaProgrammaticApproachTransaction() {
+ TransferDTO transferDTO = new TransferDTO();
+ transferDTO.setAccountFrom(ACCOUNT_NUMBER_MIGUEL);
+ transferDTO.setAccountTo(ACCOUNT_NUMBER_LOPE);
+ transferDTO.setAmount(100);
+
+ given()
+ .contentType(ContentType.JSON)
+ .body(transferDTO).post("/transfer/transaction")
+ .then().statusCode(HttpStatus.SC_CREATED);
+
+ AccountEntity miguelAccount = getAccount(ACCOUNT_NUMBER_MIGUEL);
+ Assertions.assertEquals(0, miguelAccount.getAmount(), "Unexpected amount on source account.");
+
+ AccountEntity lopeAccount = getAccount(ACCOUNT_NUMBER_LOPE);
+ Assertions.assertEquals(200, lopeAccount.getAmount(), "Unexpected amount on source account.");
+
+ JournalEntity miguelJournal = getLatestJournalRecord(ACCOUNT_NUMBER_MIGUEL);
+ Assertions.assertEquals(100, miguelJournal.getAmount(), "Unexpected journal amount.");
+ }
+
+ @Tag("QUARKUS-2492")
+ @Test
+ public void verifyNarayanaLambdaApproachTransaction() {
+ TransferDTO transferDTO = new TransferDTO();
+ transferDTO.setAccountFrom(ACCOUNT_NUMBER_GARCILASO);
+ transferDTO.setAccountTo(ACCOUNT_NUMBER_GARCILASO);
+ transferDTO.setAmount(100);
+
+ given()
+ .contentType(ContentType.JSON)
+ .body(transferDTO).post("/transfer/top-up")
+ .then().statusCode(HttpStatus.SC_CREATED);
+
+ AccountEntity garcilasoAccount = getAccount(ACCOUNT_NUMBER_GARCILASO);
+ Assertions.assertEquals(200, garcilasoAccount.getAmount(),
+ "Unexpected account amount. Expected 200 found " + garcilasoAccount.getAmount());
+
+ JournalEntity garcilasoJournal = getLatestJournalRecord(ACCOUNT_NUMBER_GARCILASO);
+ Assertions.assertEquals(100, garcilasoJournal.getAmount(), "Unexpected journal amount.");
+ }
+
+ @Tag("QUARKUS-2492")
+ @Test
+ public void verifyRollbackForNarayanaProgrammaticApproach() {
+ TransferDTO transferDTO = new TransferDTO();
+ transferDTO.setAccountFrom(ACCOUNT_NUMBER_LUIS);
+ transferDTO.setAccountTo(ACCOUNT_NUMBER_LUIS);
+ transferDTO.setAmount(200);
+
+ given()
+ .contentType(ContentType.JSON)
+ .body(transferDTO).post("/transfer/withdrawal")
+ .then().statusCode(HttpStatus.SC_BAD_REQUEST);
+
+ AccountEntity luisAccount = getAccount(ACCOUNT_NUMBER_LUIS);
+ Assertions.assertEquals(100, luisAccount.getAmount(), "Unexpected account amount.");
+
+ given().get("/transfer/journal/latest/" + ACCOUNT_NUMBER_LUIS)
+ .then()
+ .statusCode(HttpStatus.SC_NO_CONTENT);
+ }
+
+ @Tag("QUARKUS-2492")
+ @Test
+ public void smokeTestNarayanaProgrammaticTransactionTrace() {
+ String operationName = "/transfer/accounts/{account_id}";
+ given().get("/transfer/accounts/" + ACCOUNT_NUMBER_LUIS).then().statusCode(HttpStatus.SC_OK);
+ verifyRestRequestTraces(operationName);
+ }
+
+ @Tag("QUARKUS-2492")
+ @Test
+ public void smokeTestMetricsNarayanaProgrammaticTransaction() {
+ String metricName = "transaction_withdrawal_amount";
+ TransferDTO transferDTO = new TransferDTO();
+ transferDTO.setAccountFrom(ACCOUNT_NUMBER_FRANCISCO);
+ transferDTO.setAccountTo(ACCOUNT_NUMBER_FRANCISCO);
+ transferDTO.setAmount(20);
+
+ given()
+ .contentType(ContentType.JSON)
+ .body(transferDTO).post("/transfer/withdrawal")
+ .then().statusCode(HttpStatus.SC_CREATED);
+
+ verifyMetrics(metricName, greater(0));
+
+ // check rollback gauge
+ transferDTO.setAmount(3000);
+ double beforeRollback = getMetricsValue(metricName);
+ given()
+ .contentType(ContentType.JSON)
+ .body(transferDTO).post("/transfer/withdrawal")
+ .then().statusCode(HttpStatus.SC_BAD_REQUEST);
+ double afterRollback = getMetricsValue(metricName);
+ Assertions.assertEquals(beforeRollback, afterRollback, "Gauge should not be increased on a rollback transaction");
+ }
+
+ private AccountEntity getAccount(String accountNumber) {
+ return given().get("/transfer/accounts/" + accountNumber)
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract()
+ .body().as(AccountEntity.class);
+ }
+
+ private void verifyRestRequestTraces(String operationName) {
+ String[] operations = new String[] { operationName };
+ await().atMost(1, TimeUnit.MINUTES).pollInterval(Duration.ofSeconds(5)).untilAsserted(() -> {
+ retrieveTraces(20, "1h", "narayanaTransactions", operationName);
+ jaegerResponse.then().body("data[0].spans.operationName", containsInAnyOrder(operations));
+ });
+ }
+
+ private JournalEntity getLatestJournalRecord(String accountNumber) {
+ return given().get("/transfer/journal/latest/" + accountNumber)
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract()
+ .body().as(JournalEntity.class);
+ }
+
+ private void retrieveTraces(int pageLimit, String lookBack, String serviceName, String operationName) {
+ jaegerResponse = given().when()
+ .log().uri()
+ .queryParam("operation", operationName)
+ .queryParam("lookback", lookBack)
+ .queryParam("limit", pageLimit)
+ .queryParam("service", serviceName)
+ .get(jaeger.getTraceUrl());
+ }
+
+ private void verifyMetrics(String name, Predicate valueMatcher) {
+ await().ignoreExceptions().atMost(ASSERT_SERVICE_TIMEOUT_MINUTES, TimeUnit.MINUTES).untilAsserted(() -> {
+ String response = given().get("/q/metrics").then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract().asString();
+
+ boolean matches = false;
+ for (String line : response.split("[\r\n]+")) {
+ if (line.startsWith(name)) {
+ Double value = extractValueFromMetric(line);
+ Assertions.assertTrue(valueMatcher.test(value), "Metric " + name + " has unexpected value " + value);
+ matches = true;
+ break;
+ }
+ }
+
+ Assertions.assertTrue(matches, "Metric " + name + " not found in " + response);
+ });
+ }
+
+ private Double getMetricsValue(String name) {
+ String response = given().get("/q/metrics").then().statusCode(HttpStatus.SC_OK).extract().asString();
+ for (String line : response.split("[\r\n]+")) {
+ if (line.startsWith(name)) {
+ return extractValueFromMetric(line);
+ }
+ }
+
+ Assertions.fail("Metrics property " + name + " not found.");
+ return 0d;
+ }
+
+ private Double extractValueFromMetric(String line) {
+ return Double.parseDouble(line.substring(line.lastIndexOf(" ")));
+ }
+
+ private Predicate greater(double expected) {
+ return actual -> actual > expected;
+ }
+
+}
diff --git a/sql-db/narayana-transactions/src/test/resources/container-license-acceptance.txt b/sql-db/narayana-transactions/src/test/resources/container-license-acceptance.txt
new file mode 100644
index 000000000..1457cfad7
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/resources/container-license-acceptance.txt
@@ -0,0 +1,5 @@
+mcr.microsoft.com/mssql/server:2017-CU12
+mcr.microsoft.com/mssql/server:2019-CU10-ubuntu-20.04
+mcr.microsoft.com/mssql/server:2019-CU15-ubuntu-20.04
+mcr.microsoft.com/mssql/server:2019-latest
+mcr.microsoft.com/mssql/rhel/server:2022-latest
diff --git a/sql-db/narayana-transactions/src/test/resources/mariadb_app.properties b/sql-db/narayana-transactions/src/test/resources/mariadb_app.properties
new file mode 100644
index 000000000..83ef7569d
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/resources/mariadb_app.properties
@@ -0,0 +1,6 @@
+quarkus.datasource.db-kind=mariadb
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.MariaDB102Dialect
+quarkus.hibernate-orm.sql-load-script=mariadb_import.sql
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.opentelemetry.enabled=true
+quarkus.application.name=narayanaTransactions
\ No newline at end of file
diff --git a/sql-db/narayana-transactions/src/test/resources/mariadb_import.sql b/sql-db/narayana-transactions/src/test/resources/mariadb_import.sql
new file mode 100644
index 000000000..a4bf1ead2
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/resources/mariadb_import.sql
@@ -0,0 +1,7 @@
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (0, 'Garcilaso', 'de la Vega', 'CZ9250512252717368964232', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (1, 'Miguel', 'de Cervantes', 'SK0389852379529966291984', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (2, 'Luis', 'de Góngora', 'ES8521006742088984966816', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (3, 'Lope', 'de Vega', 'FR9317569000409377431694J37', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (5, 'Francisco', 'Quevedo', 'ES8521006742088984966817', 100, null, CURRENT_TIMESTAMP);
+
+UPDATE hibernate_sequence SET next_val = 5;
diff --git a/sql-db/narayana-transactions/src/test/resources/mssql.properties b/sql-db/narayana-transactions/src/test/resources/mssql.properties
new file mode 100644
index 000000000..895f040a0
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/resources/mssql.properties
@@ -0,0 +1,7 @@
+quarkus.datasource.db-kind=mssql
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.SQLServer2012Dialect
+quarkus.hibernate-orm.sql-load-script=mssql_import.sql
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.datasource.jdbc.additional-jdbc-properties.trustservercertificate=true
+quarkus.opentelemetry.enabled=true
+quarkus.application.name=narayanaTransactions
\ No newline at end of file
diff --git a/sql-db/narayana-transactions/src/test/resources/mssql_import.sql b/sql-db/narayana-transactions/src/test/resources/mssql_import.sql
new file mode 100644
index 000000000..bc9fd902c
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/resources/mssql_import.sql
@@ -0,0 +1,6 @@
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Garcilaso', 'de la Vega', 'CZ9250512252717368964232', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Miguel', 'de Cervantes', 'SK0389852379529966291984', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Luis', 'de Góngora', 'ES8521006742088984966816', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Lope', 'de Vega', 'FR9317569000409377431694J37', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Francisco', 'Quevedo', 'ES8521006742088984966817', 100, null, CURRENT_TIMESTAMP);
+
diff --git a/sql-db/narayana-transactions/src/test/resources/mysql.properties b/sql-db/narayana-transactions/src/test/resources/mysql.properties
new file mode 100644
index 000000000..e55179f53
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/resources/mysql.properties
@@ -0,0 +1,6 @@
+quarkus.datasource.db-kind=mysql
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.MariaDB102Dialect
+quarkus.hibernate-orm.sql-load-script=mariadb_import.sql
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.opentelemetry.enabled=true
+quarkus.application.name=narayanaTransactions
diff --git a/sql-db/narayana-transactions/src/test/resources/oracle.properties b/sql-db/narayana-transactions/src/test/resources/oracle.properties
new file mode 100644
index 000000000..e6699024d
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/resources/oracle.properties
@@ -0,0 +1,5 @@
+quarkus.datasource.db-kind=oracle
+quarkus.hibernate-orm.sql-load-script=oracle_import.sql
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.opentelemetry.enabled=true
+quarkus.application.name=narayanaTransactions
\ No newline at end of file
diff --git a/sql-db/narayana-transactions/src/test/resources/oracle_import.sql b/sql-db/narayana-transactions/src/test/resources/oracle_import.sql
new file mode 100644
index 000000000..8262c6fa8
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/resources/oracle_import.sql
@@ -0,0 +1,5 @@
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Garcilaso', 'de la Vega', 'CZ9250512252717368964232', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Miguel', 'de Cervantes', 'SK0389852379529966291984', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Luis', 'de Góngora', 'ES8521006742088984966816', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Lope', 'de Vega', 'FR9317569000409377431694J37', 100, null, CURRENT_TIMESTAMP);
+INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Francisco', 'Quevedo', 'ES8521006742088984966817', 100, null, CURRENT_TIMESTAMP);
diff --git a/sql-db/narayana-transactions/src/test/resources/test.properties b/sql-db/narayana-transactions/src/test/resources/test.properties
new file mode 100644
index 000000000..217165f4b
--- /dev/null
+++ b/sql-db/narayana-transactions/src/test/resources/test.properties
@@ -0,0 +1,4 @@
+ts.app.log.enable=true
+ts.postgresql.log.enable=true
+ts.database.openshift.use-internal-service-as-url=true
+ts.database.container.delete.image.on.stop=false
\ No newline at end of file