Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ dependencies {

// prometheus
implementation 'io.micrometer:micrometer-registry-prometheus'

// Logback Slack Appender
implementation 'com.github.maricn:logback-slack-appender:1.6.0'

// Logstash Logback Encoder for JSON logs
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'

}

ext {
Expand Down
1,482 changes: 1,482 additions & 0 deletions logs/application-2024-08-02.0.log

Large diffs are not rendered by default.

3,311 changes: 3,311 additions & 0 deletions logs/batch-logs.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ public class ServerBatchApplication {
public static void main(String[] args) {
SpringApplication.run(ServerBatchApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package site.billingwise.batch.server_batch.batch.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@RequestMapping("/api/batch")
@RequiredArgsConstructor
@Slf4j
public class BatchJobController {

private final JobLauncher jobLauncher;
private final Job jdbcGenerateInvoiceJob;
private final Job invoiceProcessingJob;
private final Job weeklyInvoiceStatisticsJob;
private final Job monthlyInvoiceStatisticsJob;

@PostMapping("/generate-invoice")
public String runJdbcGenerateInvoiceJob() {
return launchJob(jdbcGenerateInvoiceJob, "jdbcInvoice");
}

@PostMapping("/process-invoice")
public String runInvoiceProcessingJob() {
return launchJob(invoiceProcessingJob, "InvoiceProcessingJob");
}

@PostMapping("/weekly-statistics")
public String runWeeklyStatisticsJob() {
return launchJob(weeklyInvoiceStatisticsJob, "weeklyInvoiceStatisticsJob");
}

@PostMapping("/monthly-statistics")
public String runMonthlyStatisticsJob() {
return launchJob(monthlyInvoiceStatisticsJob, "monthlyInvoiceStatisticsJob");
}



private String launchJob(Job job, String jobName) {
String uuid = UUID.randomUUID().toString();
JobParameters jobParameters = new JobParametersBuilder()
.addLong(jobName, System.currentTimeMillis())
.addString("UUID", uuid)
.toJobParameters();
try {
jobLauncher.run(job, jobParameters);
return "Job " + jobName + " submitted successfully. UUID: " + uuid;
} catch (Exception e) {
log.error("Error occurred while starting job: {} with UUID: {}. Error: {}", jobName, uuid, e.getMessage());
return "Error occurred while starting job: " + jobName + ". UUID: " + uuid;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package site.billingwise.batch.server_batch.batch.generateinvoice;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.stereotype.Component;
Expand All @@ -13,9 +14,12 @@
import java.time.LocalDate;
import java.time.LocalDateTime;

import static site.billingwise.batch.server_batch.batch.util.StatusConstants.INVOICE_TYPE_MANUAL_BILLING;


@Component
@RequiredArgsConstructor
@Slf4j
public class GenerateInvoiceWriter implements ItemWriter<Contract> {

private final PaymentStatusRepository paymentStatusRepository;
Expand All @@ -34,17 +38,27 @@ public void write(Chunk<? extends Contract> chunk) throws Exception {
for (Contract contract : chunk) {
boolean exists = invoiceRepository.existsByContractAndMonthAndYear(contract, nextMonthValue, yearValue);

if(INVOICE_TYPE_MANUAL_BILLING == contract.getInvoiceType().getId()) {
continue;
}

if(contract.getIsDeleted()){
continue;
}

if (!exists) {
LocalDate setContractDate = LocalDate.of(yearValue, nextMonthValue, contract.getContractCycle());
LocalDateTime dueDate = calculateDueDate(contract, setContractDate);


Invoice invoice = Invoice.builder()
.contract(contract)
.invoiceType(contract.getInvoiceType())
.paymentType(contract.getPaymentType())
.paymentStatus(paymentStatusUnpaid)
.chargeAmount(contract.getItemPrice() * contract.getItemAmount())
.contractDate(setContractDate.atStartOfDay())
.isDeleted(false)
.dueDate(dueDate)
.build();
invoiceRepository.save(invoice);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.TaskletStep;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import site.billingwise.batch.server_batch.batch.listner.CustomRetryListener;
import site.billingwise.batch.server_batch.batch.listner.CustomSkipListener;
import site.billingwise.batch.server_batch.batch.listner.JobCompletionCheckListener;
import site.billingwise.batch.server_batch.batch.generateinvoice.rowmapper.JdbcContractRowMapper;
import site.billingwise.batch.server_batch.batch.listner.StepCompletionCheckListener;
import site.billingwise.batch.server_batch.batch.policy.backoff.CustomBackOffPolicy;
import site.billingwise.batch.server_batch.batch.policy.skip.CustomSkipPolicy;
import site.billingwise.batch.server_batch.domain.contract.Contract;

import javax.sql.DataSource;


@Configuration
@RequiredArgsConstructor
public class JdbcGenerateInvoiceJobConfig {
Expand All @@ -28,6 +35,10 @@ public class JdbcGenerateInvoiceJobConfig {
private final DataSource dataSource;
private final JdbcTemplate jdbcTemplate;
private final JobCompletionCheckListener jobCompletionCheckListener;
private final CustomRetryListener retryListener;
private final CustomSkipListener customSkipListener;
private final CustomSkipPolicy customSkipPolicy;
private final StepCompletionCheckListener stepCompletionCheckListener;

@Bean
public Job jdbcGenerateInvoiceJob(JobRepository jobRepository, Step jdbcGenerateInvoiceStep) {
Expand All @@ -38,23 +49,42 @@ public Job jdbcGenerateInvoiceJob(JobRepository jobRepository, Step jdbcGenerate
}



@Bean
public Step jdbcGenerateInvoiceStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("jdbcGenerateInvoiceStep", jobRepository)

CustomBackOffPolicy customBackOffPolicy = new CustomBackOffPolicy(1000L, 2.0, 4000L);

TaskletStep jdbcGenerateInvoiceStep = new StepBuilder("jdbcGenerateInvoiceStep", jobRepository)
.<Contract, Contract>chunk(CHUNK_SIZE, transactionManager)
.reader(jdbcContractItemReader())
.writer(jdbcContractItemWriter())
.faultTolerant()
.retry(Exception.class)
.retryLimit(2)
.backOffPolicy(customBackOffPolicy)
.listener(retryListener)
.skip(Exception.class)
.skipPolicy(customSkipPolicy)
.listener(customSkipListener)
.listener(stepCompletionCheckListener)
.build();
return jdbcGenerateInvoiceStep;
}

@Bean
public ItemReader<Contract> jdbcContractItemReader() {
String sql = """
select con.contract_id, con.invoice_type_id, con.payment_type_id, con.contract_cycle,
con.item_price, con.item_amount, con.is_deleted, con.is_subscription, con.payment_due_cycle
from contract con
where con.contract_status_id = 2 and con.is_deleted = false
""";

return new JdbcCursorItemReaderBuilder<Contract>()
.name("jdbcContractItemReader")
.fetchSize(CHUNK_SIZE)
.sql("select c.*, c.is_deleted " +
"from contract c " +
"where c.contract_status_id = 2")
.sql(sql)
.rowMapper(new JdbcContractRowMapper())
.dataSource(dataSource)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package site.billingwise.batch.server_batch.batch.generateinvoice.jdbc;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
Expand All @@ -23,14 +24,13 @@

@Component
@RequiredArgsConstructor
@Slf4j
public class JdbcGenerateInvoiceWriter implements ItemWriter<Contract> {

private final JdbcTemplate jdbcTemplate;


@Override
public void write(Chunk<? extends Contract> chunk) throws Exception {

LocalDateTime now = LocalDateTime.now();
LocalDateTime nextMonth = now.plusMonths(1);
int nextMonthValue = nextMonth.getMonthValue();
Expand All @@ -40,23 +40,19 @@ public void write(Chunk<? extends Contract> chunk) throws Exception {

List<Invoice> invoices = new ArrayList<>();

for (Contract contract : chunk) {

for(Contract contract : chunk) {
// 수동 청구면 pass(애초에 계약이 수동 청구인 경우)
if(INVOICE_TYPE_MANUAL_BILLING == contract.getInvoiceType().getId()) {
continue;
}

if(contract.getIsDeleted()){
// 수동 청구 계약은 건너뜀
if (INVOICE_TYPE_MANUAL_BILLING == contract.getInvoiceType().getId()) {
continue;
}


// 청구가 이미 만들어져 있으면, pass( 원래는 자동 청구인데, 단발성으로 청구를 생성한 경우 )
if(!invoiceExists(contract, nextMonthValue, yearValue)){
// 약정일
// 해당 월에 이미 청구서가 존재하는지 확인
if (!invoiceExists(contract, nextMonthValue, yearValue)) {
// 청구일 설정
LocalDateTime setInvoiceDate = LocalDateTime.of(yearValue, nextMonthValue, contract.getContractCycle(), 0, 0);
// 결제기한
// 납부 기한 계산
LocalDateTime payDueDate = calculateDueDate(contract, setInvoiceDate);

Invoice invoice = Invoice.builder()
Expand All @@ -73,7 +69,6 @@ public void write(Chunk<? extends Contract> chunk) throws Exception {
.build();

invoices.add(invoice);

}
}

Expand All @@ -86,8 +81,7 @@ private void bulkInsertInvoices(List<Invoice> invoices) {
String sql = "insert into invoice (contract_id, invoice_type_id, payment_type_id, payment_status_id, charge_amount, contract_date, due_date, is_deleted, created_at, updated_at)" +
" values (?, ?, ?, ?, ?, ?, ?, false, NOW(), NOW())";

jdbcTemplate.batchUpdate(sql,new BatchPreparedStatementSetter() {

jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Invoice invoice = invoices.get(i);
Expand All @@ -107,6 +101,7 @@ public int getBatchSize() {
});
}

// '대기' 상태의 PaymentStatus 조회 메서드
private PaymentStatus findPendingPaymentStatus() {
String sql = "select payment_status_id, name from payment_status where name = '대기'";
return jdbcTemplate.queryForObject(sql, (ResultSet rs, int rowNum) ->
Expand All @@ -115,15 +110,17 @@ private PaymentStatus findPendingPaymentStatus() {
.build());
}

// 납부 기한 계산 메서드
private LocalDateTime calculateDueDate(Contract contract, LocalDateTime setInvoiceDate) {
if (PAYMENT_TYPE_PAYER_PAYMENT == contract.getPaymentType().getId()) {
return setInvoiceDate.plusDays(3);
return setInvoiceDate.plusDays(contract.getPaymentDueCycle());
}
return setInvoiceDate;
}

// 해당 월에 청구서가 이미 존재하는지 확인
private boolean invoiceExists(Contract contract, int month, int year) {
LocalDateTime startDate = LocalDateTime.of(year, month, 1, 0, 0,0);
LocalDateTime startDate = LocalDateTime.of(year, month, 1, 0, 0, 0);
LocalDateTime endDate = startDate.plusMonths(1).minusSeconds(1);

String sql = "select count(*) from invoice where contract_id = ? " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,33 @@

import org.springframework.jdbc.core.RowMapper;
import site.billingwise.batch.server_batch.domain.contract.Contract;
import site.billingwise.batch.server_batch.domain.contract.ContractStatus;
import site.billingwise.batch.server_batch.domain.contract.PaymentType;
import site.billingwise.batch.server_batch.domain.invoice.InvoiceType;
import site.billingwise.batch.server_batch.domain.item.Item;
import site.billingwise.batch.server_batch.domain.member.Member;


import java.sql.ResultSet;
import java.sql.SQLException;

public class JdbcContractRowMapper implements RowMapper<Contract> {
@Override
public Contract mapRow(ResultSet rs, int rowNum) throws SQLException {

Member member = Member.builder()
.id(rs.getLong("member_id"))
.build();


Item item = Item.builder()
.id(rs.getLong("item_id"))
.build();


InvoiceType invoiceType = InvoiceType.builder()
.id(rs.getLong("invoice_type_id"))
.build();


PaymentType paymentType = PaymentType.builder()
.id(rs.getLong("payment_type_id"))
.build();

ContractStatus contractStatus = ContractStatus.builder()
.id(rs.getLong("contract_status_id"))
.build();


return Contract.builder()
.id(rs.getLong("contract_id"))
.member(member)
.item(item)
.invoiceType(invoiceType)
.paymentType(paymentType)
.contractStatus(contractStatus)
.paymentDueCycle(rs.getInt("payment_due_cycle"))
.isSubscription(rs.getBoolean("is_subscription"))
.itemPrice(rs.getLong("item_price"))
.itemAmount(rs.getInt("item_amount"))
.contractCycle(rs.getInt("contract_cycle"))
.paymentDueCycle(rs.getInt("payment_due_cycle"))
.isEasyConsent(rs.getBoolean("is_easy_consent"))
.isDeleted(rs.getBoolean("is_deleted"))
.build();
}
Expand Down
Loading