Skip to content
This repository has been archived by the owner on Dec 4, 2024. It is now read-only.

Commit

Permalink
Merge pull request #28 from 7SOATSquad30/feat/add-payment-option-qr-code
Browse files Browse the repository at this point in the history
feat: add payment option with qr code
  • Loading branch information
MuriloKakazu authored Jul 24, 2024
2 parents bc3bb52 + 3627bb6 commit 3ce5a11
Show file tree
Hide file tree
Showing 44 changed files with 1,024 additions and 18 deletions.
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ down:
test:
./gradlew test

install:
./gradlew build --refresh-dependencies

build:
./gradlew build

run/docker:
docker-compose up --build api

run:
docker-compose up api
./gradlew bootRun

debug:
./gradlew bootRun --debug-jvm
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
implementation 'org.apache.httpcomponents:httpclient:4.5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ services:
- database
build: .
environment:
SPRING_PROFILES_ACTIVE: "dev"
SPRING_PROFILES_ACTIVE: "dev-docker"
ports:
- "8080:8080"
networks:
Expand Down
2 changes: 2 additions & 0 deletions init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ create table tb_category (created_at TIMESTAMP WITHOUT TIME ZONE, deleted_at TIM
create table tb_customer (created_at TIMESTAMP WITHOUT TIME ZONE, deleted_at TIMESTAMP WITHOUT TIME ZONE, id bigserial not null, updated_at TIMESTAMP WITHOUT TIME ZONE, cpf varchar(255) not null unique, email varchar(255) not null, name varchar(255) not null, primary key (id));
create table tb_order (created_at TIMESTAMP WITHOUT TIME ZONE, customer_id bigint, deleted_at TIMESTAMP WITHOUT TIME ZONE, id bigserial not null, updated_at TIMESTAMP WITHOUT TIME ZONE, status varchar(255) check (status in ('DRAFT','SUBMITTED','PREPARING','READY','DELIVERED','CANCELED')), primary key (id));
create table tb_order_item (total_price float(53) not null check (total_price>=0), id bigserial not null, order_id bigint not null, product_id bigint not null, quantity bigint not null check (quantity>=1), primary key (id));
create table tb_payment (amount float(53) not null check (amount>=0), created_at TIMESTAMP WITHOUT TIME ZONE, deleted_at TIMESTAMP WITHOUT TIME ZONE, id bigserial not null, order_id bigint not null unique, updated_at TIMESTAMP WITHOUT TIME ZONE, status varchar(255) check (status in ('NOT_SUBMITTED','PROCESSING','REJECTED','COLLECTED')), primary key (id));
create table tb_product (price float(53) not null, category_id bigint not null, created_at TIMESTAMP WITHOUT TIME ZONE, deleted_at TIMESTAMP WITHOUT TIME ZONE, id bigserial not null, updated_at TIMESTAMP WITHOUT TIME ZONE, description TEXT not null, img_url varchar(255) not null, name varchar(255) not null, primary key (id));
alter table if exists tb_order add constraint FKqcp43jdylvf2riad5s1x1i2dn foreign key (customer_id) references tb_customer;
alter table if exists tb_order_item add constraint FKgeobgl2xu916he8vhljktwxnx foreign key (order_id) references tb_order;
alter table if exists tb_order_item add constraint FK4h5xid5qehset7qwe5l9c997x foreign key (product_id) references tb_product;
alter table if exists tb_payment add constraint FKokaf4il2cwit4h780c25dv04r foreign key (order_id) references tb_order;
alter table if exists tb_product add constraint FK8i0sq9mfbpsrabrm2pum9fspo foreign key (category_id) references tb_category;
INSERT INTO tb_category (name, created_at) VALUES ('Snacks', NOW());
INSERT INTO tb_category (name, created_at) VALUES ('Drinks', NOW());
Expand Down
12 changes: 12 additions & 0 deletions scripts/create-mercado-pago-point-of-sale.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
curl -X POST \
'https://api.mercadopago.com/pos'\
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $MERCADOPAGO_PRIVATE_ACCESS_TOKEN" \
-d '{
"category": 5611203,
"external_id": "STORE00001POS001",
"external_store_id": "STORE00001",
"fixed_amount": true,
"name": "First POS",
"store_id": 62212711
}'
61 changes: 61 additions & 0 deletions scripts/create-mercado-pago-store.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
curl -X POST \
'https://api.mercadopago.com/users/1910105219/stores'\
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $MERCADOPAGO_PRIVATE_ACCESS_TOKEN" \
-d '{
"business_hours": {
"monday": [
{
"open": "08:00",
"close": "23:00"
}
],
"tuesday": [
{
"open": "08:00",
"close": "23:00"
}
],
"wednesday": [
{
"open": "08:00",
"close": "23:00"
}
],
"thursday": [
{
"open": "08:00",
"close": "23:00"
}
],
"friday": [
{
"open": "08:00",
"close": "23:00"
}
],
"saturday": [
{
"open": "13:00",
"close": "22:00"
}
],
"sunday": [
{
"open": "13:00",
"close": "22:00"
}
]
},
"external_id": "STORE00001",
"location": {
"street_number": "1106",
"street_name": "Av. Paulista",
"city_name": "São Paulo",
"state_name": "São Paulo",
"latitude": -23.5640485,
"longitude": -46.6526571,
"reference": ""
},
"name": "Fastfood FIAP"
}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package br.com.fiap.grupo30.fastfood.adapters.in.rest;

import br.com.fiap.grupo30.fastfood.application.dto.CollectPaymentViaCashRequest;
import br.com.fiap.grupo30.fastfood.application.dto.OrderDTO;
import br.com.fiap.grupo30.fastfood.application.dto.PaymentQrCodeDTO;
import br.com.fiap.grupo30.fastfood.domain.usecases.payment.CollectOrderPaymentViaCashUseCase;
import br.com.fiap.grupo30.fastfood.domain.usecases.payment.GeneratePaymentQrCodeUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/payments")
@Tag(name = "Payments Resource", description = "RESTful API for managing payments.")
public class PaymentResource {

private final GeneratePaymentQrCodeUseCase generatePaymentQrCodeUseCase;
private final CollectOrderPaymentViaCashUseCase collectOrderPaymentViaCashUseCase;

@Autowired
public PaymentResource(
GeneratePaymentQrCodeUseCase generatePaymentQrCodeUseCase,
CollectOrderPaymentViaCashUseCase collectOrderPaymentViaCashUseCase) {
this.generatePaymentQrCodeUseCase = generatePaymentQrCodeUseCase;
this.collectOrderPaymentViaCashUseCase = collectOrderPaymentViaCashUseCase;
}

@PostMapping(value = "/{orderId}/qrcode")
@Operation(summary = "Generate qrcode for order payment collection")
public ResponseEntity<PaymentQrCodeDTO> generateQrCodeForPaymentCollection(
@PathVariable Long orderId) {
PaymentQrCodeDTO qrCode = this.generatePaymentQrCodeUseCase.execute(orderId);
return ResponseEntity.ok().body(qrCode);
}

@PostMapping(value = "/{orderId}/collect")
@Operation(summary = "Collect payment by cash")
public ResponseEntity<OrderDTO> collectPaymentByBash(
@PathVariable Long orderId, @RequestBody CollectPaymentViaCashRequest request) {
var order = this.collectOrderPaymentViaCashUseCase.execute(orderId, request);
return ResponseEntity.ok().body(order);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package br.com.fiap.grupo30.fastfood.adapters.in.rest;

import br.com.fiap.grupo30.fastfood.application.dto.mercadopago.events.MercadoPagoAction;
import br.com.fiap.grupo30.fastfood.application.dto.mercadopago.events.MercadoPagoActionEventDTO;
import br.com.fiap.grupo30.fastfood.domain.usecases.payment.CollectOrderPaymentViaMercadoPagoUseCase;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/webhooks")
@Tag(name = "Wehooks Resource", description = "RESTful API for webhook events.")
@SuppressWarnings("PMD.InvalidLogMessageFormat")
public class WebhookResource {

private static final Logger LOG = LoggerFactory.getLogger(WebhookResource.class);

private final CollectOrderPaymentViaMercadoPagoUseCase collectOrderPaymentMercadoPago;

@Autowired
public WebhookResource(
CollectOrderPaymentViaMercadoPagoUseCase collectOrderPaymentMercadoPago) {
this.collectOrderPaymentMercadoPago = collectOrderPaymentMercadoPago;
}

@PostMapping(value = "/mercadopago")
@Operation(summary = "Handle mercadopago events")
public ResponseEntity<String> handleMercadoPagoEvent(@RequestBody Map<String, Object> payload)
throws Exception {

// TODO: validate request authenticity

try {
// topic event
if (payload.containsKey("topic") && !payload.containsKey("action")) {
LOG.info("Ignoring topic event from mercadopago", payload);
}
// action event
else if (payload.containsKey("action") && !payload.containsKey("topic")) {
LOG.info("Received action event from mercadopago", payload);
MercadoPagoActionEventDTO actionEvent =
new ObjectMapper().convertValue(payload, MercadoPagoActionEventDTO.class);
if (!"payment".equals(actionEvent.getType())) {
LOG.info("Ignoring action event from mercadopago", payload);
return ResponseEntity.noContent().build();
}

this.handlePaymentEvent(actionEvent);
}
// unknown event
else {
LOG.warn("Ignoring unknown event from mercadopago", payload);
return ResponseEntity.badRequest().body("Unsupported mercadopago event");
}

return ResponseEntity.noContent().build();
} catch (Exception e) {
LOG.error("Failed to consume mercadopago event", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error processing event");
}
}

private void handlePaymentEvent(MercadoPagoActionEventDTO event) {
if (MercadoPagoAction.PAYMENT_CREATED.getValue().equals(event.getAction())) {
collectOrderPaymentMercadoPago.execute(event);
} else {
LOG.warn("Ignoring unimplemented payment event", event);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package br.com.fiap.grupo30.fastfood.adapters.out.mercadopago;

import br.com.fiap.grupo30.fastfood.application.dto.OrderDTO;
import br.com.fiap.grupo30.fastfood.application.dto.mercadopago.MercadoPagoPaymentDTO;
import br.com.fiap.grupo30.fastfood.application.dto.mercadopago.MercadoPagoQrCodeDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class MercadoPagoAdapter {

@Value("${integrations.mercadopago.base-url}")
private String baseUrl;

@Value("${integrations.mercadopago.public-key}")
private String publicKey;

@Value("${integrations.mercadopago.access-token}")
private String privateAccessToken;

@Value("${integrations.mercadopago.app-user-id}")
private Long appUserId;

@Value("${integrations.mercadopago.point-of-sale-id}")
private String pointOfSaleId;

@Value("${integrations.mercadopago.notifications-url}")
private String notificationsUrl;

@Value("${integrations.mercadopago.payment-collection.user-id}")
private String userIdForPaymentCollection;

private final MercadoPagoRequestBuilder requestBuilder;

public MercadoPagoAdapter(MercadoPagoRequestBuilder requestBuilder) {
this.requestBuilder = requestBuilder;
}

public MercadoPagoQrCodeDTO createQrCodeForPaymentCollection(OrderDTO order) throws Exception {
String resourceUrlTemplate =
"https://api.mercadopago.com/instore/orders/qr/seller/collectors/%s/pos/%s/qrs";
String resourceUrl = String.format(resourceUrlTemplate, appUserId, pointOfSaleId);

var requestBody =
this.requestBuilder.buildQrCodePaymentCollectionRequest(order, notificationsUrl);

ObjectMapper mapper = new ObjectMapper();
String serializedRequestBody = mapper.writeValueAsString(requestBody);

try (HttpClient client = HttpClient.newHttpClient()) {
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(resourceUrl))
.header("Authorization", String.format("Bearer %s", privateAccessToken))
.header("Content-type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(serializedRequestBody))
.build();

HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
MercadoPagoQrCodeDTO result =
mapper.readValue(response.body(), MercadoPagoQrCodeDTO.class);

return result;
}
}

public MercadoPagoPaymentDTO getPayment(String paymentId) throws Exception {
String resourceUrlTemplate = "https://api.mercadopago.com/v1/payments/%s";
String resourceUrl = String.format(resourceUrlTemplate, paymentId);

try (HttpClient client = HttpClient.newHttpClient()) {
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(resourceUrl))
.header("Authorization", String.format("Bearer %s", privateAccessToken))
.header("Content-type", "application/json")
.GET()
.build();

HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
MercadoPagoPaymentDTO result =
new ObjectMapper().readValue(response.body(), MercadoPagoPaymentDTO.class);

return result;
}
}
}
Loading

0 comments on commit 3ce5a11

Please sign in to comment.