Skip to content

Commit a2dfd14

Browse files
authored
Merge pull request #11 from lmoraesdev/develop
Develop
2 parents e69ef32 + 5a17103 commit a2dfd14

14 files changed

Lines changed: 279 additions & 40 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
push:
55
branches: ["epic/**", develop, main]
66
pull_request:
7-
branches: [main]
7+
branches: [main, develop]
88

99
permissions:
1010
contents: read

src/main/java/com/lmoraesdev/payment/PaymentApiApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.scheduling.annotation.EnableScheduling;
56

67
@SpringBootApplication
8+
@EnableScheduling
79
public class PaymentApiApplication {
810

911
public static void main(String[] args) {

src/main/java/com/lmoraesdev/payment/adapter/in/web/ChargeController.java

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,23 @@
11
package com.lmoraesdev.payment.adapter.in.web;
22

3+
import com.lmoraesdev.payment.adapter.in.web.documentation.ChargeControllerDoc;
34
import com.lmoraesdev.payment.application.port.in.CreateCharge;
45
import com.lmoraesdev.payment.application.port.in.CreateChargeCommand;
56
import com.lmoraesdev.payment.application.port.in.CreateChargeResult;
6-
import io.swagger.v3.oas.annotations.Operation;
7-
import io.swagger.v3.oas.annotations.media.Content;
8-
import io.swagger.v3.oas.annotations.media.Schema;
9-
import io.swagger.v3.oas.annotations.responses.ApiResponse;
10-
import io.swagger.v3.oas.annotations.responses.ApiResponses;
11-
import io.swagger.v3.oas.annotations.tags.Tag;
12-
import jakarta.validation.Valid;
137
import org.springframework.http.HttpStatus;
14-
import org.springframework.http.ProblemDetail;
158
import org.springframework.http.ResponseEntity;
16-
import org.springframework.web.bind.annotation.PostMapping;
17-
import org.springframework.web.bind.annotation.RequestBody;
18-
import org.springframework.web.bind.annotation.RequestMapping;
199
import org.springframework.web.bind.annotation.RestController;
2010

21-
@Tag(name = "Charges", description = "Gerenciamento de cobranças Pix")
2211
@RestController
23-
@RequestMapping("/charges")
24-
public class ChargeController {
12+
public class ChargeController implements ChargeControllerDoc {
2513
private final CreateCharge createCharge;
2614

2715
public ChargeController(CreateCharge createCharge) {
2816
this.createCharge = createCharge;
2917
}
3018

31-
@Operation(
32-
summary = "Criar cobrança",
33-
description = "Cria uma nova cobrança Pix com status ACTIVE")
34-
@ApiResponses({
35-
@ApiResponse(
36-
responseCode = "201",
37-
description = "Cobrança criada com sucesso",
38-
content = @Content(schema = @Schema(implementation = CreateChargeResponse.class))),
39-
@ApiResponse(
40-
responseCode = "400",
41-
description = "Dados inválidos — Problem Details com erros por campo",
42-
content = @Content(schema = @Schema(implementation = ProblemDetail.class))),
43-
@ApiResponse(
44-
responseCode = "422",
45-
description = "Regra de negócio violada",
46-
content = @Content(schema = @Schema(implementation = ProblemDetail.class))),
47-
@ApiResponse(
48-
responseCode = "500",
49-
description = "Erro interno inesperado",
50-
content = @Content(schema = @Schema(implementation = ProblemDetail.class)))
51-
})
52-
@PostMapping
53-
public ResponseEntity<CreateChargeResponse> create(
54-
@Valid @RequestBody CreateChargeRequest request) {
19+
@Override
20+
public ResponseEntity<CreateChargeResponse> create(CreateChargeRequest request) {
5521
CreateChargeResult result = createCharge.create(new CreateChargeCommand(request.amount()));
5622

5723
CreateChargeResponse response =
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.lmoraesdev.payment.adapter.in.web.documentation;
2+
3+
import com.lmoraesdev.payment.adapter.in.web.CreateChargeRequest;
4+
import com.lmoraesdev.payment.adapter.in.web.CreateChargeResponse;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.media.Content;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
9+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
10+
import io.swagger.v3.oas.annotations.tags.Tag;
11+
import jakarta.validation.Valid;
12+
import org.springframework.http.ProblemDetail;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.web.bind.annotation.PostMapping;
15+
import org.springframework.web.bind.annotation.RequestBody;
16+
import org.springframework.web.bind.annotation.RequestMapping;
17+
18+
@Tag(name = "Charges", description = "Gerenciamento de cobranças Pix")
19+
@RequestMapping("/charges")
20+
public interface ChargeControllerDoc {
21+
22+
@Operation(
23+
summary = "Criar cobrança",
24+
description = "Cria uma nova cobrança Pix com status ACTIVE")
25+
@ApiResponses({
26+
@ApiResponse(
27+
responseCode = "201",
28+
description = "Cobrança criada com sucesso",
29+
content = @Content(schema = @Schema(implementation = CreateChargeResponse.class))),
30+
@ApiResponse(
31+
responseCode = "400",
32+
description = "Dados inválidos — Problem Details com erros por campo",
33+
content = @Content(schema = @Schema(implementation = ProblemDetail.class))),
34+
@ApiResponse(
35+
responseCode = "422",
36+
description = "Regra de negócio violada",
37+
content = @Content(schema = @Schema(implementation = ProblemDetail.class))),
38+
@ApiResponse(
39+
responseCode = "500",
40+
description = "Erro interno inesperado",
41+
content = @Content(schema = @Schema(implementation = ProblemDetail.class)))
42+
})
43+
@PostMapping
44+
ResponseEntity<CreateChargeResponse> create(@Valid @RequestBody CreateChargeRequest request);
45+
}

src/main/java/com/lmoraesdev/payment/adapter/out/messaging/.gitkeep

Whitespace-only changes.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.lmoraesdev.payment.adapter.out.messaging;
2+
3+
import com.lmoraesdev.payment.adapter.out.persistence.outbox.OutboxEventEntity;
4+
import com.lmoraesdev.payment.adapter.out.persistence.outbox.OutboxEventJpaRepository;
5+
import com.lmoraesdev.payment.adapter.out.persistence.outbox.OutboxStatus;
6+
import java.util.List;
7+
import java.util.UUID;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.springframework.kafka.core.KafkaTemplate;
11+
import org.springframework.scheduling.annotation.Scheduled;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.transaction.annotation.Transactional;
14+
15+
@Component
16+
public class OutboxRelay {
17+
18+
private static final Logger LOG = LoggerFactory.getLogger(OutboxRelay.class);
19+
private static final String TOPIC = "payments.charge-created";
20+
21+
private final OutboxEventJpaRepository repository;
22+
private final KafkaTemplate<Object, Object> kafkaTemplate;
23+
24+
public OutboxRelay(
25+
OutboxEventJpaRepository repository, KafkaTemplate<Object, Object> kafkaTemplate) {
26+
this.repository = repository;
27+
this.kafkaTemplate = kafkaTemplate;
28+
}
29+
30+
@Scheduled(fixedDelay = 5000)
31+
public void publishPending() {
32+
List<OutboxEventEntity> pending =
33+
repository.findTop50ByStatusOrderByCreatedAtAsc(OutboxStatus.PENDING);
34+
35+
for (OutboxEventEntity event : pending) {
36+
publish(event);
37+
}
38+
}
39+
40+
private void publish(OutboxEventEntity event) {
41+
try {
42+
kafkaTemplate.send(TOPIC, event.getAggregateId(), event.getPayload()).get();
43+
markPublished(event.getId());
44+
} catch (Exception e) {
45+
LOG.warn("failed to publish outbox event {}, will retry next poll", event.getId(), e);
46+
}
47+
}
48+
49+
@Transactional
50+
public void markPublished(UUID eventId) {
51+
repository
52+
.findById(eventId)
53+
.ifPresent(
54+
event -> {
55+
event.markPublished();
56+
repository.save(event);
57+
});
58+
}
59+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.lmoraesdev.payment.adapter.out.persistence.outbox;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.lmoraesdev.payment.application.port.out.OutboxEventPort;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class OutboxEventAdapter implements OutboxEventPort {
10+
11+
private final OutboxEventJpaRepository repository;
12+
private final ObjectMapper objectMapper;
13+
14+
public OutboxEventAdapter(OutboxEventJpaRepository repository, ObjectMapper objectMapper) {
15+
this.repository = repository;
16+
this.objectMapper = objectMapper;
17+
}
18+
19+
@Override
20+
public void record(String aggregateType, String aggregateId, String eventType, Object payload) {
21+
OutboxEventEntity event =
22+
OutboxEventEntity.pending(aggregateType, aggregateId, eventType, toJson(payload));
23+
repository.save(event);
24+
}
25+
26+
private String toJson(Object payload) {
27+
try {
28+
return objectMapper.writeValueAsString(payload);
29+
} catch (JsonProcessingException e) {
30+
throw new IllegalStateException("failed to serialize outbox payload", e);
31+
}
32+
}
33+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.lmoraesdev.payment.adapter.out.persistence.outbox;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.EnumType;
6+
import jakarta.persistence.Enumerated;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.Index;
9+
import jakarta.persistence.Table;
10+
import java.time.Instant;
11+
import java.util.UUID;
12+
import org.hibernate.annotations.JdbcTypeCode;
13+
import org.hibernate.type.SqlTypes;
14+
15+
@Entity
16+
@Table(
17+
name = "outbox_events",
18+
indexes = @Index(name = "idx_outbox_status_created_at", columnList = "status, created_at"))
19+
public class OutboxEventEntity {
20+
21+
@Id private UUID id;
22+
23+
@Column(name = "aggregate_type", nullable = false, updatable = false)
24+
private String aggregateType;
25+
26+
@Column(name = "aggregate_id", nullable = false, updatable = false)
27+
private String aggregateId;
28+
29+
@Column(name = "event_type", nullable = false, updatable = false)
30+
private String eventType;
31+
32+
@JdbcTypeCode(SqlTypes.JSON)
33+
@Column(nullable = false, updatable = false, columnDefinition = "jsonb")
34+
private String payload;
35+
36+
@Enumerated(EnumType.STRING)
37+
@Column(nullable = false)
38+
private OutboxStatus status;
39+
40+
@Column(name = "created_at", nullable = false, updatable = false)
41+
private Instant createdAt;
42+
43+
@Column(name = "published_at")
44+
private Instant publishedAt;
45+
46+
protected OutboxEventEntity() {
47+
// JPA
48+
}
49+
50+
public static OutboxEventEntity pending(
51+
String aggregateType, String aggregateId, String eventType, String payloadJson) {
52+
OutboxEventEntity event = new OutboxEventEntity();
53+
event.id = UUID.randomUUID();
54+
event.aggregateType = aggregateType;
55+
event.aggregateId = aggregateId;
56+
event.eventType = eventType;
57+
event.payload = payloadJson;
58+
event.status = OutboxStatus.PENDING;
59+
event.createdAt = Instant.now();
60+
return event;
61+
}
62+
63+
public void markPublished() {
64+
this.status = OutboxStatus.PUBLISHED;
65+
this.publishedAt = Instant.now();
66+
}
67+
68+
public UUID getId() {
69+
return id;
70+
}
71+
72+
public String getAggregateId() {
73+
return aggregateId;
74+
}
75+
76+
public String getEventType() {
77+
return eventType;
78+
}
79+
80+
public String getPayload() {
81+
return payload;
82+
}
83+
84+
public OutboxStatus getStatus() {
85+
return status;
86+
}
87+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.lmoraesdev.payment.adapter.out.persistence.outbox;
2+
3+
import java.util.List;
4+
import java.util.UUID;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
public interface OutboxEventJpaRepository extends JpaRepository<OutboxEventEntity, UUID> {
8+
9+
List<OutboxEventEntity> findTop50ByStatusOrderByCreatedAtAsc(OutboxStatus status);
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.lmoraesdev.payment.adapter.out.persistence.outbox;
2+
3+
public enum OutboxStatus {
4+
PENDING,
5+
PUBLISHED
6+
}

0 commit comments

Comments
 (0)