initial commit

This commit is contained in:
allard
2025-11-23 18:58:51 +01:00
commit 376a944abc
1553 changed files with 314731 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
FROM open-liberty:19.0.0.12-kernel-java8-openj9
USER root
RUN apt-get update && apt-get upgrade -y e2fsprogs libgnutls30 libgcrypt20 libsasl2-2
USER 1001
COPY --chown=1001:0 src/main/liberty/config/ /config/
COPY --chown=1001:0 src/main/resources/security/ /config/resources/security/
COPY --chown=1001:0 target/*.war /config/apps/
COPY --chown=1001:0 target/jdbc/* /config/jdbc/
RUN configure.sh

View File

@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: transaction-service
labels:
app: transaction-service
spec:
replicas: 1
selector:
matchLabels:
app: transaction-service
template:
metadata:
labels:
app: transaction-service
annotations:
sidecar.istio.io/inject: "false"
spec:
containers:
- name: transaction-service
image: ykoyfman/bank-transaction-service:1.0
imagePullPolicy: Always
ports:
- name: http-server
containerPort: 9080
envFrom:
- secretRef:
name: bank-db-secret
- secretRef:
name: bank-oidc-secret
env:
- name: USER_SERVICE_URL
value: "http://user-service:9080/bank/v1/users"
- name: KNATIVE_SERVICE_URL
value: "http://process-transaction.example-bank.svc.cluster.local"
- name: WLP_LOGGING_CONSOLE_LOGLEVEL
value: INFO
---
apiVersion: v1
kind: Service
metadata:
name: transaction-service
labels:
app: transaction-service
spec:
ports:
- port: 9080
targetPort: 9080
selector:
app: transaction-service
---
apiVersion: v1
kind: Route
metadata:
name: transaction-service
spec:
to:
kind: Service
name: transaction-service

View File

@@ -0,0 +1,78 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>transaction-service</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- Open Liberty Features -->
<dependency>
<groupId>io.openliberty.features</groupId>
<artifactId>microProfile-3.0</artifactId>
<type>esa</type>
</dependency>
<dependency>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
<type>jar</type>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
</plugin>
<!-- Add JDBC driver to package -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>copy-jdbc-driver</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.8</version>
<outputDirectory>${project.build.directory}/jdbc</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<!-- Plugin to run unit tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<!-- Plugin to run functional tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,25 @@
package com.ibm.codey.bank;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;
@Liveness
@ApplicationScoped
public class LivenessCheck implements HealthCheck {
private boolean isAlive() {
// perform health checks here
return true;
}
@Override
public HealthCheckResponse call() {
boolean up = isAlive();
return HealthCheckResponse.named(this.getClass().getSimpleName()).state(up).build();
}
}

View File

@@ -0,0 +1,9 @@
package com.ibm.codey.bank;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/bank")
public class LoyaltyApplication extends Application {
}

View File

@@ -0,0 +1,25 @@
package com.ibm.codey.bank;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;
@Readiness
@ApplicationScoped
public class ReadinessCheck implements HealthCheck {
private boolean isReady() {
// perform readiness checks, e.g. database connection, etc.
return true;
}
@Override
public HealthCheckResponse call() {
boolean up = isReady();
return HealthCheckResponse.named(this.getClass().getSimpleName()).state(up).build();
}
}

View File

@@ -0,0 +1,189 @@
package com.ibm.codey.bank.catalog;
import java.net.URL;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.interceptor.Interceptors;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import com.ibm.codey.bank.BaseResource;
import com.ibm.codey.bank.accounts.json.UserRegistration;
import com.ibm.codey.bank.accounts.json.UserRegistrationInfo;
import com.ibm.codey.bank.catalog.dao.TransactionDao;
import com.ibm.codey.bank.catalog.json.CreateTransactionDefinition;
import com.ibm.codey.bank.catalog.json.RewardTransactionDefinition;
import com.ibm.codey.bank.catalog.models.Category;
import com.ibm.codey.bank.catalog.models.Transaction;
import com.ibm.codey.bank.interceptor.LoggingInterceptor;
import com.ibm.codey.bank.interceptor.binding.RequiresAuthorization;
@RequestScoped
@Interceptors(LoggingInterceptor.class)
@Path("v1/transactions")
public class TransactionResource extends BaseResource {
@Inject
private TransactionDao transactionDao;
@Inject
@ConfigProperty(name = "USER_SERVICE_URL")
private URL userServiceURL;
/**
* This method creates a transaction.
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Transactional
public Response createTransaction(CreateTransactionDefinition createTransactionDefinition) {
Transaction newTransaction = new Transaction();
// create new uuid for new transaction
String transactionId = UUID.randomUUID().toString();
// get subject
String subject = this.getCallerSubject();
// get user
UserService userService = RestClientBuilder.newBuilder().baseUrl(userServiceURL).build(UserService.class);
try {
UserRegistrationInfo userRegistration = userService.getUserConsent(this.getCallerCredentials());
if (!userRegistration.isConsentGiven()) {
return Response.status(Response.Status.CONFLICT).entity("User has not consented to program").build();
}
newTransaction.setTransactionId(transactionId);
newTransaction.setUserId(userRegistration.getUserId());
newTransaction.setTransactionName(createTransactionDefinition.getTransactionName());
newTransaction.setCategory(createTransactionDefinition.getCategory());
newTransaction.setAmount(createTransactionDefinition.getAmount());
newTransaction.setProcessed(false);
newTransaction.setDate(OffsetDateTime.now());
transactionDao.createTransaction(newTransaction);
return Response.status(Response.Status.NO_CONTENT).build();
} catch(WebApplicationException wae) {
int status = wae.getResponse().getStatus();
if (status == Response.Status.NOT_FOUND.getStatusCode()) {
return Response.status(Response.Status.NOT_FOUND).entity("User not registered").build();
} else {
wae.printStackTrace();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
}
/**
* This method gets the transactions of a user.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public Response getTransactions() {
// get subject
String subject = this.getCallerSubject();
// get user
UserService userService = RestClientBuilder.newBuilder().baseUrl(userServiceURL).build(UserService.class);
try {
UserRegistrationInfo userRegistration = userService.getUserConsent(this.getCallerCredentials());
if (!userRegistration.isConsentGiven()) {
return Response.status(Response.Status.CONFLICT).entity("User has not consented to program").build();
}
List<Transaction> transactions = transactionDao.findTransactionsByUser(userRegistration.getUserId());
return Response.status(Response.Status.OK).entity(transactions).build();
} catch(WebApplicationException wae) {
int status = wae.getResponse().getStatus();
if (status == Response.Status.NOT_FOUND.getStatusCode()) {
return Response.status(Response.Status.NOT_FOUND).entity("User not registered").build();
} else {
wae.printStackTrace();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
}
/**
* This method gets the spending categories of a user.
*/
@GET
@Path("spending")
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public Response getCategory() {
// get subject
String subject = this.getCallerSubject();
// get user
UserService userService = RestClientBuilder.newBuilder().baseUrl(userServiceURL).build(UserService.class);
try {
UserRegistrationInfo userRegistration = userService.getUserConsent(this.getCallerCredentials());
if (!userRegistration.isConsentGiven()) {
return Response.status(Response.Status.CONFLICT).entity("User has not consented to program").build();
}
List<Category> categories = transactionDao.groupCategoriesForUser(userRegistration.getUserId());
return Response.status(Response.Status.OK).entity(categories).build();
} catch(WebApplicationException wae) {
int status = wae.getResponse().getStatus();
if (status == Response.Status.NOT_FOUND.getStatusCode()) {
return Response.status(Response.Status.NOT_FOUND).entity("User not registered").build();
} else {
wae.printStackTrace();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
}
// TODO: require admin scope
/**
* This method updates a transaction.
*/
@PUT
@Path("reward/{transactionId}")
@Consumes(MediaType.APPLICATION_JSON)
@Transactional
@RequiresAuthorization
public Response updateTransaction(@PathParam("transactionId") String transactionId, RewardTransactionDefinition rewardTransactionDefinition) {
// Validate UUID is formatted correctly.
try {
UUID.fromString(transactionId);
} catch(IllegalArgumentException iae) {
return Response.status(Response.Status.BAD_REQUEST).entity("Invalid transaction id").build();
}
Transaction transaction = transactionDao.findTransactionById(transactionId);
if (transaction == null) {
return Response.status(Response.Status.NOT_FOUND).entity("Transaction not found").build();
}
if (transaction.isProcessed()) {
return Response.status(Response.Status.BAD_REQUEST).entity("Transaction already processed").build();
}
transaction.setPointsEarned(rewardTransactionDefinition.getPointsEarned());
transaction.setProcessed(true);
transactionDao.updateTransaction(transaction);
return Response.status(Response.Status.NO_CONTENT).build();
}
}

View File

@@ -0,0 +1,79 @@
package com.ibm.codey.bank.catalog.dao;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import com.ibm.codey.bank.catalog.models.Category;
import com.ibm.codey.bank.catalog.models.Transaction;
@RequestScoped
public class TransactionDao {
@PersistenceContext(name = "jpa-unit")
private EntityManager em;
public void createTransaction(Transaction transaction) {
em.persist(transaction);
}
public void updateTransaction(Transaction transaction) {
em.merge(transaction);
}
public List<Transaction> findTransactions() {
return em.createNamedQuery("Transaction.findTransactions", Transaction.class)
.getResultList();
}
public List<Transaction> findTransactionsByUser(String userId) {
return em.createNamedQuery("Transaction.findTransactionsByUser", Transaction.class)
.setParameter("userId", userId)
.getResultList();
}
public Transaction findTransactionById(String transactionId) {
try {
return em.createNamedQuery("Transaction.findTransactionByIdOnly", Transaction.class)
.setParameter("transactionId", transactionId)
.getSingleResult();
} catch(NoResultException e) {
return null;
}
}
public Transaction findTransactionById(String transactionId, String userId) {
try {
return em.createNamedQuery("Transaction.findTransactionById", Transaction.class)
.setParameter("transactionId", transactionId)
.setParameter("userId", userId)
.getSingleResult();
} catch(NoResultException e) {
return null;
}
}
public List<Category> groupCategoriesForUser(String userId) {
try {
List<Object[][]> rows = em.createNamedQuery("Transaction.groupCategoriesForUser", Object[][].class)
.setParameter("userId", userId)
.getResultList();
List<Category> response = new ArrayList<>();
for (Object[] row: rows) {
if (row.length == 2) {
response.add(new Category(row[0].toString(), new BigDecimal(row[1].toString())));
}
}
return response;
} catch(NoResultException e) {
return null;
}
}
}

View File

@@ -0,0 +1,18 @@
package com.ibm.codey.bank.catalog.models;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class Category {
private String category;
private BigDecimal amount;
public Category(String category, BigDecimal amount) {
this.category = category;
this.amount = amount;
}
}

View File

@@ -0,0 +1,64 @@
package com.ibm.codey.bank.catalog.models;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "transactions")
@IdClass(TransactionPK.class)
@NamedQueries({
@NamedQuery(name = "Transaction.findTransactions", query = "SELECT t FROM Transaction t"),
@NamedQuery(name = "Transaction.findTransactionsByUser", query = "SELECT t FROM Transaction t WHERE t.userId = :userId"),
@NamedQuery(name = "Transaction.findTransactionById", query = "SELECT t FROM Transaction t WHERE t.transactionId = :transactionId AND t.userId = :userId"),
@NamedQuery(name = "Transaction.findTransactionByIdOnly", query = "SELECT t FROM Transaction t WHERE t.transactionId = :transactionId"),
@NamedQuery(name = "Transaction.groupCategoriesForUser", query = "SELECT COALESCE(t.category, 'Uncategorized'), SUM (t.amount) FROM Transaction t WHERE t.userId = :userId GROUP BY t.category")
})
@Getter @Setter
@EntityListeners(TransactionListener.class)
public class Transaction implements Serializable {
private static final long serialVersionUID = 1L;
@Column(name = "transaction_id")
@Id
private String transactionId;
@Id
@Column(name = "usr")
private String userId;
@Column(name = "transaction_name")
private String transactionName;
@Column(name = "amount")
private BigDecimal amount;
@Column(name = "category")
private String category;
@Column(name = "points_earned")
private BigDecimal pointsEarned;
@Column(name = "processed")
private boolean processed;
@Column(name = "date")
private OffsetDateTime date;
public Transaction() {
}
}

View File

@@ -0,0 +1,40 @@
package com.ibm.codey.bank.catalog.models;
import java.net.URL;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.persistence.PostPersist;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import com.ibm.codey.bank.catalog.KnativeService;
@RequestScoped
public class TransactionListener {
@Inject
@ConfigProperty(name = "KNATIVE_SERVICE_URL")
private URL knativeServiceURL;
@PostPersist
public void sendToProcessing(Transaction transaction) {
KnativeService knativeService = RestClientBuilder.newBuilder().baseUrl(knativeServiceURL).build(KnativeService.class);
try {
knativeService.processTransaction(transaction.getTransactionId(), transaction.getCategory(), transaction.getAmount().toString());
} catch (WebApplicationException wae) {
System.out.print("web app exception");
int status = wae.getResponse().getStatus();
if (status == Response.Status.NOT_FOUND.getStatusCode()) {
// TODO: ..
} else {
wae.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,15 @@
package com.ibm.codey.bank.catalog.models;
import java.io.Serializable;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class TransactionPK implements Serializable {
private String transactionId;
private String userId;
}

View File

@@ -0,0 +1,2 @@
default.http.port=9080
default.https.port=9443

View File

@@ -0,0 +1,2 @@
# This option is needed when using an IBM JRE to avoid a handshake failure when making a secure JDBC connection.
-Dcom.ibm.jsse2.overrideDefaultTLS=true

View File

@@ -0,0 +1,53 @@
<server description="Liberty server">
<featureManager>
<feature>jpa-2.2</feature>
<feature>microProfile-3.0</feature>
<feature>mpJwt-1.1</feature>
</featureManager>
<logging traceSpecification="eclipselink=all" maxFileSize="20" maxFiles="10"/>
<keyStore id="digicertRootCA" password="digicert" location="${server.config.dir}/resources/security/digicert-root-ca.jks"/>
<ssl id="defaultSSLConfig" keyStoreRef="defaultKeyStore" trustStoreRef="digicertRootCA" />
<httpEndpoint host="*" httpPort="${default.http.port}"
httpsPort="${default.https.port}" id="defaultHttpEndpoint"/>
<mpJwt
id="jwt"
issuer="${OIDC_ISSUERIDENTIFIER}"
jwksUri="${OIDC_JWKENDPOINTURL}"
audiences="${OIDC_AUDIENCES}"
userNameAttribute="sub"
/>
<library id="PostgresLib">
<fileset dir="${server.config.dir}/jdbc"/>
</library>
<dataSource id="AccountsDataSource" jndiName="jdbc/AccountsDataSource">
<jdbcDriver libraryRef="PostgresLib" />
<!-- Idle connections to this server are timing out after 5 minutes.
It is recommended to set maxIdleTime to half of that value to avoid jdbc failures (e.g. broken pipe).
Reap time is reduced from default of 3 minutes to close idle connections in time. -->
<connectionManager maxIdleTime="2m30s" reapTime="60s"/>
<properties.postgresql
serverName="${DB_SERVERNAME}"
portNumber="${DB_PORTNUMBER}"
databaseName="${DB_DATABASENAME}"
user="${DB_USER}"
password="${DB_PASSWORD}"
ssl="false"
/>
</dataSource>
<webApplication location="transaction-service.war" contextRoot="/">
<application-bnd>
<security-role name="authenticated">
<special-subject type="ALL_AUTHENTICATED_USERS"/>
</security-role>
</application-bnd>
</webApplication>
</server>

View File

@@ -0,0 +1,10 @@
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd"
version="2.0">
<persistence-unit-metadata>
<persistence-unit-defaults>
<schema>bank</schema>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="jpa-unit" transaction-type="JTA">
<jta-data-source>jdbc/AccountsDataSource</jta-data-source>
<shared-cache-mode>NONE</shared-cache-mode>
<properties>
<property name="eclipselink.target-database" value="PostgreSQL"/>
<property name="eclipselink.logging.level" value="ALL"/>
<property name="eclipselink.logging.parameters" value="true"/>
</properties>
</persistence-unit>
</persistence>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>

View File

@@ -0,0 +1,27 @@
<web-app
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>transaction-service</display-name>
<security-role>
<role-name>authenticated</role-name>
</security-role>
<security-constraint>
<display-name>Security Constraints</display-name>
<web-resource-collection>
<web-resource-name>ProtectedArea</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>authenticated</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>NONE</transport-guarantee>
</user-data-constraint>
</security-constraint>
</web-app>