- 파서: 입금→수입, 출금/이체→지출, 카드 승인→지출, 카드 취소/환불→수입(환불) 은행 거래는 은행(BANK) 지갑에, 카드는 카드(CARD) 지갑에 매칭(matchWalletId 일반화) - createFromNotification: 파싱된 수입/지출 타입·지갑종류로 생성 - 파서 테스트 갱신(은행 입금/출금 케이스 포함) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -220,20 +220,21 @@ public class AccountService {
|
|||||||
public AccountEntryResponse createFromNotification(NotificationRequest req, Long memberId) {
|
public AccountEntryResponse createFromNotification(NotificationRequest req, Long memberId) {
|
||||||
CardNotificationParser.Parsed p =
|
CardNotificationParser.Parsed p =
|
||||||
notificationParser.parse(req.getTitle(), req.getText(), java.time.LocalDate.now());
|
notificationParser.parse(req.getTitle(), req.getText(), java.time.LocalDate.now());
|
||||||
if (!p.isCard() || p.isCanceled() || p.getAmount() <= 0) {
|
if (!p.isRecognized() || p.getAmount() <= 0) {
|
||||||
return null; // 결제 승인 알림 아님 / 취소 → 무시
|
return null; // 결제/이체 알림 아님 → 무시
|
||||||
}
|
}
|
||||||
// 중복방지
|
// 중복방지
|
||||||
if (p.getNotifKey() != null && mapper.findByMemberAndNotifKey(memberId, p.getNotifKey()) != null) {
|
if (p.getNotifKey() != null && mapper.findByMemberAndNotifKey(memberId, p.getNotifKey()) != null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 카드사 → 등록된 카드(CARD) 매칭
|
// 카드 결제 → 카드(CARD) 매칭 / 은행 입출금·이체 → 은행(BANK) 매칭
|
||||||
Long walletId = matchCardWalletId(p.getIssuer(), memberId);
|
String walletType = p.isBankTransfer() ? "BANK" : "CARD";
|
||||||
|
Long walletId = matchWalletId(p.getIssuer(), memberId, walletType);
|
||||||
|
|
||||||
AccountEntry entry = AccountEntry.builder()
|
AccountEntry entry = AccountEntry.builder()
|
||||||
.memberId(memberId)
|
.memberId(memberId)
|
||||||
.entryDate(p.getDate() != null ? p.getDate() : java.time.LocalDate.now())
|
.entryDate(p.getDate() != null ? p.getDate() : java.time.LocalDate.now())
|
||||||
.type("EXPENSE")
|
.type(p.isIncome() ? "INCOME" : "EXPENSE")
|
||||||
.category(null)
|
.category(null)
|
||||||
.amount(p.getAmount())
|
.amount(p.getAmount())
|
||||||
.memo(p.getMerchant())
|
.memo(p.getMerchant())
|
||||||
@@ -260,21 +261,21 @@ public class AccountService {
|
|||||||
return toResponse(mapper.findByIdAndMember(id, memberId));
|
return toResponse(mapper.findByIdAndMember(id, memberId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 카드사명으로 등록된 CARD 지갑 id 찾기 (issuer/name 양방향 부분일치). 미매칭이면 카드가 1개뿐일 때 그 카드로. */
|
/** 카드사/은행명으로 등록된 해당 type(CARD/BANK) 지갑 id 찾기 (issuer/name 유연 매칭).
|
||||||
private Long matchCardWalletId(String issuer, Long memberId) {
|
* 미매칭이라도 그 type 지갑이 정확히 1개면 그 지갑으로 기본 매칭. */
|
||||||
List<Wallet> cards = walletMapper.findByMember(memberId).stream()
|
private Long matchWalletId(String issuer, Long memberId, String walletType) {
|
||||||
.filter(w -> "CARD".equals(w.getType()))
|
List<Wallet> wallets = walletMapper.findByMember(memberId).stream()
|
||||||
|
.filter(w -> walletType.equals(w.getType()))
|
||||||
.toList();
|
.toList();
|
||||||
if (issuer != null) {
|
if (issuer != null) {
|
||||||
Long matched = cards.stream()
|
Long matched = wallets.stream()
|
||||||
.filter(w -> overlaps(w.getIssuer(), issuer) || overlaps(w.getName(), issuer))
|
.filter(w -> overlaps(w.getIssuer(), issuer) || overlaps(w.getName(), issuer))
|
||||||
.map(Wallet::getId)
|
.map(Wallet::getId)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
if (matched != null) return matched;
|
if (matched != null) return matched;
|
||||||
}
|
}
|
||||||
// 카드사 미감지/미매칭이라도 등록된 카드가 정확히 1개면 그 카드로 기본 매칭
|
return wallets.size() == 1 ? wallets.get(0).getId() : null;
|
||||||
return cards.size() == 1 ? cards.get(0).getId() : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 표기 차이를 흡수한 카드사 매칭 (KB국민 ↔ 국민 ↔ 국민카드 ↔ KB카드 등) */
|
/** 표기 차이를 흡수한 카드사 매칭 (KB국민 ↔ 국민 ↔ 국민카드 ↔ KB카드 등) */
|
||||||
|
|||||||
@@ -10,66 +10,80 @@ import java.util.regex.Matcher;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카드사 결제 푸시 알림 텍스트 파싱.
|
* 결제/이체 푸시 알림 텍스트 파싱.
|
||||||
* 카드사마다 문구가 달라 휴리스틱으로 카드사·금액·가맹점·승인/취소를 추출한다.
|
* - 카드: 승인 → 지출, 취소/환불 → 수입(환불)
|
||||||
|
* - 은행: 입금 → 수입, 출금/이체 → 지출
|
||||||
|
* 문구가 제각각이라 휴리스틱으로 종류·수입/지출·카드사(은행)·금액·가맹점·날짜를 추출한다.
|
||||||
* (서버에 두어 APK 재빌드 없이 규칙 보강 가능)
|
* (서버에 두어 APK 재빌드 없이 규칙 보강 가능)
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class CardNotificationParser {
|
public class CardNotificationParser {
|
||||||
|
|
||||||
// 감지할 카드사 (긴 이름 우선)
|
// 감지할 카드사/은행 (긴 이름 우선)
|
||||||
private static final List<String> ISSUERS = List.of(
|
private static final List<String> ISSUERS = List.of(
|
||||||
"KB국민", "국민", "삼성", "신한", "현대", "롯데", "하나", "우리", "비씨", "BC",
|
"KB국민", "국민", "삼성", "신한", "현대", "롯데", "하나", "우리", "비씨", "BC",
|
||||||
"농협", "NH", "씨티", "카카오뱅크", "카카오", "토스", "수협", "광주", "전북", "제주"
|
"농협", "NH", "씨티", "카카오뱅크", "카카오", "토스", "수협", "광주", "전북", "제주",
|
||||||
|
"기업", "산업", "새마을", "신협", "우체국", "케이뱅크", "K뱅크"
|
||||||
);
|
);
|
||||||
|
|
||||||
private static final Pattern AMOUNT = Pattern.compile("([0-9]{1,3}(?:,[0-9]{3})+|[0-9]{4,})\\s*원");
|
private static final Pattern AMOUNT = Pattern.compile("([0-9]{1,3}(?:,[0-9]{3})+|[0-9]{4,})\\s*원");
|
||||||
private static final Pattern DATE_MD = Pattern.compile("(\\d{1,2})[./](\\d{1,2})");
|
private static final Pattern DATE_MD = Pattern.compile("(\\d{1,2})[./](\\d{1,2})");
|
||||||
|
|
||||||
// 광고/이벤트 푸시에 흔한 표현 — 강한 승인신호(승인/출금)가 없으면 결제로 보지 않는다.
|
// 광고/이벤트 푸시에 흔한 표현 — 강한 신호(승인/입금/출금/이체/취소/환불)가 없으면 무시.
|
||||||
private static final Pattern AD_HINT = Pattern.compile(
|
private static final Pattern AD_HINT = Pattern.compile(
|
||||||
"이벤트|광고|혜택|쿠폰|할인|적립|포인트|캐시백|마일리지|리워드|당첨|응모|추첨|프로모션|배너|무료|증정|모집");
|
"이벤트|광고|혜택|쿠폰|할인|적립|포인트|캐시백|마일리지|리워드|당첨|응모|추첨|프로모션|배너|무료|증정|모집");
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
public static class Parsed {
|
public static class Parsed {
|
||||||
private boolean card; // 카드 결제 알림으로 인식되었는가
|
private boolean recognized; // 가계부에 넣을 알림으로 인식되었는가
|
||||||
private boolean canceled; // 취소/취소승인
|
private boolean income; // true=수입(입금/환불), false=지출(승인/출금/이체)
|
||||||
private String issuer; // 감지된 카드사 (없으면 null)
|
private boolean bankTransfer; // true=은행 입출금/이체, false=카드 결제
|
||||||
private long amount; // 금액(원)
|
private String issuer; // 감지된 카드사/은행 (없으면 null)
|
||||||
private String merchant; // 가맹점(추정)
|
private long amount; // 금액(원)
|
||||||
private LocalDate date; // 거래일(추정, 없으면 null)
|
private String merchant; // 카드:가맹점 / 은행:적요(추정)
|
||||||
private String notifKey; // 중복방지 키
|
private LocalDate date; // 거래일(추정, 없으면 오늘)
|
||||||
|
private String notifKey; // 중복방지 키
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 카드 결제 알림 파싱. card=false 면 결제 알림이 아님(무시 대상). */
|
/** 알림 파싱. recognized=false 면 가계부 대상 아님(무시). */
|
||||||
public Parsed parse(String title, String text, LocalDate today) {
|
public Parsed parse(String title, String text, LocalDate today) {
|
||||||
String raw = ((title == null ? "" : title) + "\n" + (text == null ? "" : text)).trim();
|
String raw = ((title == null ? "" : title) + "\n" + (text == null ? "" : text)).trim();
|
||||||
if (raw.isBlank()) return notCard();
|
if (raw.isBlank()) return notRecognized();
|
||||||
|
|
||||||
|
boolean deposit = raw.contains("입금");
|
||||||
|
boolean withdraw = raw.contains("출금") || raw.contains("이체") || raw.contains("송금");
|
||||||
|
boolean refund = raw.contains("취소") || raw.contains("환불");
|
||||||
|
boolean approval = raw.contains("승인") || raw.contains("결제") || raw.contains("일시불") || raw.contains("할부");
|
||||||
|
|
||||||
boolean approved = raw.contains("승인") || raw.contains("결제") || raw.contains("출금");
|
|
||||||
boolean canceled = raw.contains("취소") || raw.contains("환불");
|
|
||||||
Matcher am = AMOUNT.matcher(raw);
|
Matcher am = AMOUNT.matcher(raw);
|
||||||
if (!am.find() || (!approved && !canceled)) {
|
boolean strong = approval || deposit || withdraw || refund;
|
||||||
return notCard();
|
if (!am.find() || !strong) {
|
||||||
|
return notRecognized();
|
||||||
}
|
}
|
||||||
// 광고/이벤트 차단: 강한 승인신호(승인/출금/취소/환불)가 없는데 광고성 표현이 있으면 결제 아님
|
// 광고/이벤트 차단: (강신호는 위에서 보장되지만) 광고성 표현이 섞인 약한 케이스 방지용
|
||||||
boolean strongSignal = raw.contains("승인") || raw.contains("출금") || canceled;
|
if (!strong && AD_HINT.matcher(raw).find()) {
|
||||||
if (!strongSignal && AD_HINT.matcher(raw).find()) {
|
return notRecognized();
|
||||||
return notCard();
|
|
||||||
}
|
}
|
||||||
long amount = Long.parseLong(am.group(1).replace(",", ""));
|
long amount = Long.parseLong(am.group(1).replace(",", ""));
|
||||||
|
|
||||||
|
// 수입/지출: 입금·취소·환불 = 수입, 그 외(승인/결제/출금/이체) = 지출
|
||||||
|
boolean income = deposit || refund;
|
||||||
|
// 은행 이체성: 카드 승인 신호 없이 입금/출금/이체 → 은행 거래(카드사 대신 은행 계좌에 매칭)
|
||||||
|
boolean bankTransfer = (deposit || withdraw) && !approval;
|
||||||
|
|
||||||
String issuer = detectIssuer(raw);
|
String issuer = detectIssuer(raw);
|
||||||
String merchant = guessMerchant(raw);
|
String merchant = guessMerchant(raw);
|
||||||
LocalDate date = guessDate(raw, today);
|
LocalDate date = guessDate(raw, today);
|
||||||
|
|
||||||
String notifKey = (issuer == null ? "?" : issuer) + "|" + amount + "|"
|
String notifKey = (issuer == null ? "?" : issuer) + "|" + amount + "|"
|
||||||
+ (merchant == null ? "" : merchant) + "|" + date + "|" + (canceled ? "C" : "A");
|
+ (merchant == null ? "" : merchant) + "|" + date + "|"
|
||||||
|
+ (income ? "I" : "E") + (bankTransfer ? "B" : "C");
|
||||||
|
|
||||||
return Parsed.builder()
|
return Parsed.builder()
|
||||||
.card(true)
|
.recognized(true)
|
||||||
.canceled(canceled)
|
.income(income)
|
||||||
|
.bankTransfer(bankTransfer)
|
||||||
.issuer(issuer)
|
.issuer(issuer)
|
||||||
.amount(amount)
|
.amount(amount)
|
||||||
.merchant(merchant)
|
.merchant(merchant)
|
||||||
@@ -78,8 +92,8 @@ public class CardNotificationParser {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Parsed notCard() {
|
private Parsed notRecognized() {
|
||||||
return Parsed.builder().card(false).build();
|
return Parsed.builder().recognized(false).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String detectIssuer(String raw) {
|
private String detectIssuer(String raw) {
|
||||||
@@ -108,7 +122,7 @@ public class CardNotificationParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 가맹점 추정: 잡음(카드사·승인문구·금액·날짜·시간·마스킹 이름 등)을 제거한 뒤
|
* 가맹점/적요 추정: 잡음(카드사·문구·금액·날짜·시간·마스킹 이름 등)을 제거한 뒤
|
||||||
* 남은 한글/영문 토큰 중 가장 그럴듯한 것을 고른다. (완벽하지 않음 — 사용자가 확정 시 수정)
|
* 남은 한글/영문 토큰 중 가장 그럴듯한 것을 고른다. (완벽하지 않음 — 사용자가 확정 시 수정)
|
||||||
*/
|
*/
|
||||||
private String guessMerchant(String raw) {
|
private String guessMerchant(String raw) {
|
||||||
@@ -118,12 +132,12 @@ public class CardNotificationParser {
|
|||||||
.replaceAll("\\d{1,2}[:시]\\d{2}분?", " ") // 시간
|
.replaceAll("\\d{1,2}[:시]\\d{2}분?", " ") // 시간
|
||||||
.replaceAll("\\d{1,2}[./]\\d{1,2}", " ") // 날짜
|
.replaceAll("\\d{1,2}[./]\\d{1,2}", " ") // 날짜
|
||||||
.replaceAll("\\([^)]*\\)", " ") // 괄호(카드 끝번호 등)
|
.replaceAll("\\([^)]*\\)", " ") // 괄호(카드 끝번호 등)
|
||||||
.replaceAll("승인취소|취소승인|승인|결제|취소|환불|일시불|할부|누적|잔액|출금|입금|체크|신용", " ")
|
.replaceAll("승인취소|취소승인|승인|결제|취소|환불|일시불|할부|누적|잔액|출금|입금|이체|송금|체크|신용", " ")
|
||||||
.replaceAll("[가-힣]\\*+[가-힣]?", " "); // 마스킹 이름(홍*동)
|
.replaceAll("[가-힣]\\*+[가-힣]?", " "); // 마스킹 이름(홍*동)
|
||||||
for (String c : ISSUERS) {
|
for (String c : ISSUERS) {
|
||||||
s = s.replace(c, " ");
|
s = s.replace(c, " ");
|
||||||
}
|
}
|
||||||
s = s.replace("카드", " ");
|
s = s.replace("카드", " ").replace("은행", " ");
|
||||||
String[] tokens = s.trim().split("\\s+");
|
String[] tokens = s.trim().split("\\s+");
|
||||||
String best = null;
|
String best = null;
|
||||||
for (String t : tokens) {
|
for (String t : tokens) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import java.time.LocalDate;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카드 결제 알림 파서 단위테스트.
|
* 결제/이체 알림 파서 단위테스트.
|
||||||
* 핵심: 정상 결제 인식 / 취소 인식 / 광고성 푸시 차단 / 강신호 우선.
|
* 카드: 승인→지출 / 취소→수입(환불). 은행: 입금→수입 / 출금·이체→지출. 광고 차단.
|
||||||
*/
|
*/
|
||||||
class CardNotificationParserTest {
|
class CardNotificationParserTest {
|
||||||
|
|
||||||
@@ -17,74 +17,79 @@ class CardNotificationParserTest {
|
|||||||
private final LocalDate today = LocalDate.of(2026, 6, 6);
|
private final LocalDate today = LocalDate.of(2026, 6, 6);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("정상 승인 결제는 카드로 인식하고 카드사·금액을 추출한다")
|
@DisplayName("카드 승인 = 지출(카드)")
|
||||||
void parsesApprovedPayment() {
|
void cardApprovalIsExpense() {
|
||||||
var p = parser.parse("[Web발신]", "삼성카드 승인 50,000원 일시불\n스타벅스 06/06 14:30", today);
|
var p = parser.parse("[Web발신]", "삼성카드 승인 50,000원 일시불\n스타벅스 06/06 14:30", today);
|
||||||
|
assertThat(p.isRecognized()).isTrue();
|
||||||
assertThat(p.isCard()).isTrue();
|
assertThat(p.isIncome()).isFalse();
|
||||||
assertThat(p.isCanceled()).isFalse();
|
assertThat(p.isBankTransfer()).isFalse();
|
||||||
assertThat(p.getIssuer()).isEqualTo("삼성");
|
assertThat(p.getIssuer()).isEqualTo("삼성");
|
||||||
assertThat(p.getAmount()).isEqualTo(50000L);
|
assertThat(p.getAmount()).isEqualTo(50000L);
|
||||||
assertThat(p.getNotifKey()).isNotBlank();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("승인취소는 취소 결제로 인식한다")
|
@DisplayName("카드 승인취소 = 수입(환불, 카드)")
|
||||||
void parsesCanceledPayment() {
|
void cardCancelIsIncome() {
|
||||||
var p = parser.parse("현대카드", "승인취소 12,000원", today);
|
var p = parser.parse("현대카드", "승인취소 12,000원", today);
|
||||||
|
assertThat(p.isRecognized()).isTrue();
|
||||||
assertThat(p.isCard()).isTrue();
|
assertThat(p.isIncome()).isTrue();
|
||||||
assertThat(p.isCanceled()).isTrue();
|
assertThat(p.isBankTransfer()).isFalse();
|
||||||
assertThat(p.getIssuer()).isEqualTo("현대");
|
assertThat(p.getIssuer()).isEqualTo("현대");
|
||||||
assertThat(p.getAmount()).isEqualTo(12000L);
|
assertThat(p.getAmount()).isEqualTo(12000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("강한 승인신호 없이 광고성 표현만 있으면 결제로 보지 않는다")
|
@DisplayName("은행 입금 = 수입(은행)")
|
||||||
void blocksAdvertisementPush() {
|
void bankDepositIsIncome() {
|
||||||
var p = parser.parse("카카오", "결제하고 혜택 받기! 10,000원 쿠폰 적립 이벤트", today);
|
var p = parser.parse("[Web발신]", "신한은행 입금 300,000원 홍길동", today);
|
||||||
|
assertThat(p.isRecognized()).isTrue();
|
||||||
assertThat(p.isCard()).isFalse();
|
assertThat(p.isIncome()).isTrue();
|
||||||
|
assertThat(p.isBankTransfer()).isTrue();
|
||||||
|
assertThat(p.getIssuer()).isEqualTo("신한");
|
||||||
|
assertThat(p.getAmount()).isEqualTo(300000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("승인 신호가 있으면 광고 표현이 섞여도 결제로 인식한다")
|
@DisplayName("은행 출금/이체 = 지출(은행)")
|
||||||
void strongSignalOverridesAdWords() {
|
void bankWithdrawIsExpense() {
|
||||||
var p = parser.parse("삼성카드", "승인 30,000원 (이벤트 적립 대상)", today);
|
var p = parser.parse("국민은행", "출금 45,000원 이체", today);
|
||||||
|
assertThat(p.isRecognized()).isTrue();
|
||||||
assertThat(p.isCard()).isTrue();
|
assertThat(p.isIncome()).isFalse();
|
||||||
assertThat(p.getAmount()).isEqualTo(30000L);
|
assertThat(p.isBankTransfer()).isTrue();
|
||||||
|
assertThat(p.getAmount()).isEqualTo(45000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("금액이 없으면 결제 알림이 아니다")
|
@DisplayName("광고성 푸시(강신호 없음)는 무시")
|
||||||
|
void blocksAdvertisement() {
|
||||||
|
var p = parser.parse("카카오", "혜택 쿠폰 10,000원 적립 이벤트", today);
|
||||||
|
assertThat(p.isRecognized()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("금액 없으면 무시")
|
||||||
void requiresAmount() {
|
void requiresAmount() {
|
||||||
var p = parser.parse("삼성카드", "승인 완료되었습니다", today);
|
assertThat(parser.parse("삼성카드", "승인 완료되었습니다", today).isRecognized()).isFalse();
|
||||||
|
|
||||||
assertThat(p.isCard()).isFalse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("승인/취소 키워드가 없으면 결제 알림이 아니다")
|
@DisplayName("승인/입금/출금 등 신호 없으면 무시")
|
||||||
void requiresApprovalKeyword() {
|
void requiresSignal() {
|
||||||
var p = parser.parse("삼성카드", "50,000원", today);
|
assertThat(parser.parse("삼성카드", "50,000원", today).isRecognized()).isFalse();
|
||||||
|
|
||||||
assertThat(p.isCard()).isFalse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("빈 알림은 결제가 아니다")
|
@DisplayName("빈 알림은 무시")
|
||||||
void blankIsNotCard() {
|
void blankIsNotRecognized() {
|
||||||
assertThat(parser.parse(null, null, today).isCard()).isFalse();
|
assertThat(parser.parse(null, null, today).isRecognized()).isFalse();
|
||||||
assertThat(parser.parse("", "", today).isCard()).isFalse();
|
assertThat(parser.parse("", "", today).isRecognized()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("MM/DD 날짜를 올해 거래일로 추정한다")
|
@DisplayName("MM/DD 날짜를 올해 거래일로 추정")
|
||||||
void guessesDate() {
|
void guessesDate() {
|
||||||
var p = parser.parse("신한카드", "승인 5,000원 6/6", today);
|
var p = parser.parse("신한카드", "승인 5,000원 6/6", today);
|
||||||
|
assertThat(p.isRecognized()).isTrue();
|
||||||
assertThat(p.isCard()).isTrue();
|
|
||||||
assertThat(p.getDate()).isEqualTo(LocalDate.of(2026, 6, 6));
|
assertThat(p.getDate()).isEqualTo(LocalDate.of(2026, 6, 6));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user