Skip to content

Commit

Permalink
Merge pull request #305 from Sunagatov/feature/stripe-integration
Browse files Browse the repository at this point in the history
Stripe POC
  • Loading branch information
Sunagatov authored Jun 15, 2024
2 parents f615f6a + b8bea57 commit ec8393c
Show file tree
Hide file tree
Showing 18 changed files with 356 additions and 12 deletions.
10 changes: 6 additions & 4 deletions Dockerfile.local
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
FROM maven:3.9-eclipse-temurin-21-alpine AS build
WORKDIR /usr/app
COPY . /usr/app
RUN mvn versions:set-property -Dproperty=project.version -DnewVersion=0.0.1 && \
mvn package -Pdev -DskipTests
ENV HOME=/usr/app
WORKDIR $HOME
COPY pom.xml $HOME
RUN mvn verify clean --fail-never
COPY . $HOME
RUN --mount=type=cache,target=/root/.m2 mvn package -Pdev -DskipTests

FROM maven:3.9-eclipse-temurin-21-alpine
WORKDIR /usr/app
Expand Down
42 changes: 41 additions & 1 deletion START.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Key variables which are used in the startup of the app. They are pre-configured
- `AWS_PRODUCT_BUCKET` AWS product's bucket name
- `AWS_USER_BUCKET` AWS product's bucket name
- `AWS_DEFAULT_PRODUCT_IMAGES_PATH` Package with product's files
- `STRIPE_SECRET_KEY` Stripe secret key for payment integration

Refer to [docker-compose.local.yml](./docker-compose.local.yml)

Expand Down Expand Up @@ -74,6 +75,45 @@ docker-compose -f docker-compose.local.yml logs [iced-latte-backend|iced-latte-p
docker-compose -f docker-compose.local.yml down -v
```

### Remote Debugging (e.g. in Docker)
If you want to debug BE application running in Docker, use **Remote JVM Debug** configuration:
1. Double press **Shift**
2. Type `Edit Configurations`
3. Click `+` and select `Remote JVM Debug`
4. Select **Attach to Remote JVM**, make sure that port is `5005` and host is `localhost`
5. Save configuration and click debug button
6. Start containers as usual `docker compose -f docker-compose.local.yml up -d --build`
7. Set a breakpoint, e.g. here [ProductsEndpoint#getProducts](src/main/java/com/zufar/icedlatte/product/endpoint/ProductsEndpoint.java#L67)
8. Try it out:

```bash
curl -X 'GET' \
'http://localhost:8083/api/v1/products?page=0&size=50&sort_attribute=name&sort_direction=desc' \
-H 'accept: application/json'
```

Enjoy!

![](docs/images/remote_debug.png)

### Local Frontend + Backend
To run FE and BE locally for testing purposes:
1. Check out [Iced Latte Frontend](https://github.com/Sunagatov/Iced-Latte-Frontend/) repo
2. Navigate to the root of FE repo and create `.env`: ```bash echo 'NEXT_PUBLIC_API_HOST_REMOTE=http://localhost:80/backend/api/v1' > .env```
3. Uncomment `iced-latte-frontend.build` section in [docker-compose.local.yml](./docker-compose.local.yml#L18)
4. Set path to local FE repo in `iced-latte-frontend.build.context`
5. Run build as usual
```bash
docker compose -f docker-compose.local.yml up -d --build
```

* FE is here http://localhost/
* BE is here http://localhost/backend (e.g. http://localhost/backend/api/v1/products) and here too http://localhost:8083

:warning: **Limitations**:

AWS is available only in production, therefore there are no real pictures of products, only stubs.

## Database Navigator

> For Ultimate Edition consider using [Database Tools and SQL plugin](https://www.jetbrains.com/help/idea/relational-databases.html)
Expand All @@ -90,4 +130,4 @@ Add new PostgresSQL connection:

Enjoy!

![](db_navigator.png)
![](docs/images/db_navigator.png)
8 changes: 5 additions & 3 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ services:
iced-latte-frontend:
image: 'zufarexplainedit/iced-latte-frontend:latest'
container_name: iced-latte-frontend
environment:
- .env
# build:
# context: /Users/astriganova/Desktop/Projects/Iced-Latte-Frontend
# dockerfile: Dockerfile.local
restart: 'always'
ports:
- '3000:3000'
Expand Down Expand Up @@ -46,12 +47,13 @@ services:
GOOGLE_AUTH_CLIENT_ID: vbfgngfdndgndgndgndgndgndgndg
GOOGLE_AUTH_CLIENT_SECRET: vbfgngfdndgndgndgndgndgndgndg
GOOGLE_AUTH_REDIRECT_URI: vbfgngfdndgndgndgndgndgndgndg
STRIPE_SECRET_KEY: sk_test_51PJxciHA4AopuQMMeXaJNETc7RUAITeMTKJei07L8iEHrRiWLQalKsr756dnOzmKPUXufkUVNUSaiPyktJG9dGY500x0cM817f
build:
context: .
dockerfile: Dockerfile.local
ports:
- '8083:8083'
- '5005:5005'
- '5005:5005' # port for remote debugging
networks:
- iced-latte-network
depends_on:
Expand Down
File renamed without changes
Binary file added docs/images/remote_debug.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions docs/plantuml/payment/checkout-sequence.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@startuml
'https://plantuml.com/sequence-diagram

autonumber

title Checkout Sequence, Logged-In User
participant "Frontend" as fe
participant "Backend" as be
participant "Stripe" as stripe
database "PostgreSQL" as db

fe -> be: GET /api/v1/payment
be -> db: Create new order (status CREATED)
be -> stripe: Create session and POST /v1/checkout/sessions
be <-- stripe: Return client_secret
be -> db: Save client_secret
be --> fe: Return client_secret
fe -> stripe: Embed Stripe's payment page using client_secret
fe <[#red]-- stripe: Redirect /orders?sessionId={CHECKOUT_SESSION_ID} <font color=red><b>REDIRECT TO USER!!!
fe -> be: GET /stripe/session-status?sessionId={CHECKOUT_SESSION_ID}
group <font color=red><b>!!! Inconsistency danger: what if step 6 doesn't happen
be -> stripe: GET /v1/checkout/sessions/CHECKOUT_SESSION_ID
be <-- stripe: Session status (OPEN / COMPLETE / EXPIRED)
be -> db: Update order status if COMPLETE (status PAID)
fe <-- be: Response with order status and id
end
@enduml
15 changes: 15 additions & 0 deletions docs/plantuml/payment/order-state.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@startuml

title Order States

[*] --> CREATED : BE receives payment request
CREATED -right-> PAID : successful payment
CREATED --> UNSUCCESSFUL_PAYMENT : rejected payment
UNSUCCESSFUL_PAYMENT --> PAID : successful payment
UNSUCCESSFUL_PAYMENT -> UNSUCCESSFUL_PAYMENT : rejected payment

CREATED: New Order
PAID : Stripe redirected success=true
UNSUCCESSFUL_PAYMENT : Stripe redirected success=false

@enduml
23 changes: 21 additions & 2 deletions nginx_data/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,34 @@ upstream frontend {
server {
listen 80;

location / {
location /backend/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# proxy_intercept_errors off; # Ensure NGINX does not intercept redirects
# add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.stripe.com; frame-src 'self' https://js.stripe.com https://checkout.stripe.com;";

# Add CORS headers
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}

if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
}
}

location /frontend/ {
location / {
proxy_pass http://frontend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

<properties>
<!-- Project Version -->
<project.version>${project.version}</project.version>
<project.version>0.0.1</project.version>

<!-- Encoding -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ public ShoppingCartDto updateProductQuantityInShoppingCartItem(final UUID shoppi
final int productQuantityChange) throws ShoppingCartNotFoundException, ShoppingCartItemNotFoundException, InvalidShoppingCartIdException {
return productQuantityItemUpdater.update(shoppingCartItemId, productQuantityChange);
}

public void deleteByUserId(final UUID userId) {
shoppingCartProvider.deleteByUserId(userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ public ShoppingCartDto getByUserId(final UUID userId) {
}
return shoppingCartDtoConverter.toDto(shoppingCart);
}


@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public void deleteByUserId(final UUID userId) {
ShoppingCart shoppingCart = shoppingCartRepository.findShoppingCartByUserId(userId);
shoppingCartRepository.delete(shoppingCart);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.zufar.icedlatte.payment.api;

import com.stripe.param.checkout.SessionCreateParams;
import com.zufar.icedlatte.openapi.dto.ShoppingCartDto;
import com.zufar.icedlatte.openapi.dto.ShoppingCartItemDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Service
public class StripeLineItemsConverter {

// FIXME: use JPA entities instead of converted DTO
public List<SessionCreateParams.LineItem> getLineItems(ShoppingCartDto shoppingCart) {
var result = new ArrayList<SessionCreateParams.LineItem>();
for (ShoppingCartItemDto item : shoppingCart.getItems()) {
var quantity = item.getProductQuantity();
// convert to cents
var unitAmount = item.getProductInfo().getPrice().multiply(BigDecimal.valueOf(100)).longValue();
var productName = item.getProductInfo().getName();
var productData = SessionCreateParams.LineItem.PriceData.ProductData.builder()
.setName(productName)
.build();
var priceData = SessionCreateParams.LineItem.PriceData.builder()
.setUnitAmount(unitAmount)
.setCurrency("USD")
.setProductData(productData)
.build();
var lineItem = SessionCreateParams.LineItem.builder()
.setQuantity((long) quantity)
.setPriceData(priceData)
.build();
result.add(lineItem);
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.zufar.icedlatte.payment.api;

import com.stripe.param.checkout.SessionCreateParams;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Service
public class StripeShippingOptionsProvider {

public List<SessionCreateParams.ShippingOption> getShippingOptions() {
var result = new ArrayList<SessionCreateParams.ShippingOption>();
var option1 = SessionCreateParams.ShippingOption.builder()
.setShippingRateData(
SessionCreateParams.ShippingOption.ShippingRateData.builder()
.setType(
SessionCreateParams.ShippingOption.ShippingRateData.Type.FIXED_AMOUNT
)
.setFixedAmount(
SessionCreateParams.ShippingOption.ShippingRateData.FixedAmount.builder()
.setAmount(0L)
.setCurrency("usd")
.build()
)
.setDisplayName("Free shipping")
.setDeliveryEstimate(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.builder()
.setMinimum(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.Minimum.builder()
.setUnit(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.Minimum.Unit.BUSINESS_DAY
)
.setValue(5L)
.build()
)
.setMaximum(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.Maximum.builder()
.setUnit(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.Maximum.Unit.BUSINESS_DAY
)
.setValue(7L)
.build()
)
.build()
)
.build()
)
.build();
result.add(option1);
var option2 = SessionCreateParams.ShippingOption.builder()
.setShippingRateData(
SessionCreateParams.ShippingOption.ShippingRateData.builder()
.setType(
SessionCreateParams.ShippingOption.ShippingRateData.Type.FIXED_AMOUNT
)
.setFixedAmount(
SessionCreateParams.ShippingOption.ShippingRateData.FixedAmount.builder()
.setAmount(1500L)
.setCurrency("usd")
.build()
)
.setDisplayName("Next day air")
.setDeliveryEstimate(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.builder()
.setMinimum(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.Minimum.builder()
.setUnit(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.Minimum.Unit.BUSINESS_DAY
)
.setValue(1L)
.build()
)
.setMaximum(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.Maximum.builder()
.setUnit(
SessionCreateParams.ShippingOption.ShippingRateData.DeliveryEstimate.Maximum.Unit.BUSINESS_DAY
)
.setValue(1L)
.build()
)
.build()
)
.build()
)
.build();
result.add(option2);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.zufar.icedlatte.payment.dto;

public record PaymentSession(String clientSecret) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.zufar.icedlatte.payment.dto;

public record PaymentSessionStatus(String status, String customerEmail) {
}
Loading

0 comments on commit ec8393c

Please sign in to comment.