import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; /** Standalone regression checks; does not connect to a database. */ public final class RepairSkuDurationTest { private static int checks; public static void main(String[] args) { exactYearIsCandidateForBothSkus(); calendarMonthArithmeticHandlesLeapDaysAndMonthEnds(); timestampDifferencesRequireReview(); missingDatesAndOtherDurationsAreSkipped(); unrelatedSkusAreSkipped(); renewalAndMultiplePurchasesRequireReview(); quantityMeansDifferentThingsForLegacyAndSlotOrders(); missingOrMismatchedPurchaseRequiresReview(); adjustedAndSuspendedTicketsRequireReview(); specialAcquisitionRequiresReview(); generatedSqlMatchesTheOldSnapshot(); nullableUpdatedSnapshotIsPreserved(); System.out.println("RepairSkuDurationTest: " + checks + " checks passed"); } private static void exactYearIsCandidateForBothSkus() { for (long skuId : new long[]{772L, 774L}) { RepairSkuDuration.Ticket ticket = ticket(); ticket.skuId = skuId; RepairSkuDuration.Decision decision = classify(ticket); kind("candidate", decision, "exact year, SKU " + skuId); equal(ticket.expiry.minusMonths(6), decision.newExpiry, "candidate subtracts six calendar months from expiry"); equal(0.0, decision.deltaSeconds, "exact year has no timestamp difference"); } RepairSkuDuration.Ticket outside = ticket(); outside.status = "outside"; kind("candidate", classify(outside), "occupied extra seat is supported"); } private static void calendarMonthArithmeticHandlesLeapDaysAndMonthEnds() { RepairSkuDuration.Ticket leap = ticket(); leap.start = time("2024-02-29T10:20:30"); leap.expiry = time("2025-02-28T10:20:30"); RepairSkuDuration.Decision leapDecision = classify(leap); kind("candidate", leapDecision, "leap-day year uses calendar arithmetic"); equal(time("2024-08-28T10:20:30"), leapDecision.newExpiry, "subtract from actual expiry; do not calculate start plus six months"); RepairSkuDuration.Ticket monthEnd = ticket(); monthEnd.start = time("2025-08-31T10:20:30"); monthEnd.expiry = time("2026-08-31T10:20:30"); RepairSkuDuration.Decision monthEndDecision = classify(monthEnd); kind("candidate", monthEndDecision, "month-end year is recognized"); equal(time("2026-02-28T10:20:30"), monthEndDecision.newExpiry, "six-month subtraction clamps to the last day of February"); RepairSkuDuration.Ticket leapFebruary = ticket(); leapFebruary.start = time("2023-08-31T10:20:30"); leapFebruary.expiry = time("2024-08-31T10:20:30"); equal(time("2024-02-29T10:20:30"), classify(leapFebruary).newExpiry, "six-month subtraction preserves leap February's final day"); } private static void timestampDifferencesRequireReview() { for (long seconds : new long[]{-1, 1, -30, 30}) { RepairSkuDuration.Ticket ticket = ticket(); ticket.expiry = ticket.expiry.plusSeconds(seconds); RepairSkuDuration.Decision decision = classify(ticket); kind("review", decision, "timestamp difference " + seconds + " seconds"); equal(Math.abs((double) seconds), Math.abs(decision.deltaSeconds), "report the timestamp difference accurately"); } RepairSkuDuration.Ticket manualTime = ticket(); manualTime.expiry = manualTime.expiry.toLocalDate().atStartOfDay(); kind("review", classify(manualTime), "same anniversary date with a different time"); } private static void missingDatesAndOtherDurationsAreSkipped() { RepairSkuDuration.Ticket noStart = ticket(); noStart.start = null; kind("skipped", classify(noStart), "missing start time"); RepairSkuDuration.Ticket noExpiry = ticket(); noExpiry.expiry = null; kind("skipped", classify(noExpiry), "missing expiry time"); RepairSkuDuration.Ticket noDates = ticket(); noDates.start = null; noDates.expiry = null; kind("skipped", classify(noDates), "ticket has never been activated"); for (int months : new int[]{1, 6, 18, 24}) { RepairSkuDuration.Ticket ticket = ticket(); ticket.expiry = ticket.start.plusMonths(months); kind("skipped", classify(ticket), "nonannual duration " + months + " months"); } } private static void unrelatedSkusAreSkipped() { for (long skuId : new long[]{773L, 775L, 1L}) { RepairSkuDuration.Ticket ticket = ticket(); ticket.skuId = skuId; kind("skipped", classify(ticket), "unrelated SKU " + skuId); } } private static void renewalAndMultiplePurchasesRequireReview() { RepairSkuDuration.Ticket ticket = ticket(); RepairSkuDuration.Purchase renewal = purchase(ticket); renewal.orderType = 2; kind("review", classify(ticket, Collections.singletonList(renewal), 0), "a renewal must not be mistaken for an incorrectly configured first purchase"); RepairSkuDuration.Purchase first = purchase(ticket); RepairSkuDuration.Purchase second = purchase(ticket); second.id++; kind("review", classify(ticket, Arrays.asList(first, second), 0), "two purchases may legitimately add up to a year"); second.orderType = 2; kind("review", classify(ticket, Arrays.asList(first, second), 0), "original purchase plus renewal requires review"); } private static void quantityMeansDifferentThingsForLegacyAndSlotOrders() { RepairSkuDuration.Ticket ticket = ticket(); RepairSkuDuration.Purchase purchase = purchase(ticket); purchase.quantity = 2; kind("review", classify(ticket, Collections.singletonList(purchase), 0), "legacy purchase quantity can extend one ticket's duration"); purchase.slot = true; RepairSkuDuration.Decision decision = classify(ticket, Collections.singletonList(purchase), 0); kind("candidate", decision, "a successful delivery slot represents one ticket"); equal(ticket.expiry.minusMonths(6), decision.newExpiry, "slot delivery subtracts six months once, regardless of order quantity"); } private static void missingOrMismatchedPurchaseRequiresReview() { RepairSkuDuration.Ticket ticket = ticket(); kind("review", classify(ticket, Collections.emptyList(), 0), "missing purchase does not prove that the annual duration is wrong"); RepairSkuDuration.Purchase purchase = purchase(ticket); purchase.skuId = 775L; kind("review", classify(ticket, Collections.singletonList(purchase), 0), "current ticket SKU differs from the purchase SKU"); } private static void adjustedAndSuspendedTicketsRequireReview() { RepairSkuDuration.Ticket ticket = ticket(); kind("review", classify(ticket, Collections.singletonList(purchase(ticket)), 1), "manual adjustment history requires review even if the dates total one year"); RepairSkuDuration.Ticket compensated = ticket(); compensated.compensationDays = 3; kind("review", classify(compensated), "compensated ticket"); RepairSkuDuration.Ticket frozen = ticket(); frozen.frozen = true; kind("review", classify(frozen), "frozen ticket"); RepairSkuDuration.Ticket recovering = ticket(); recovering.recover = true; kind("review", classify(recovering), "ticket being restored"); RepairSkuDuration.Ticket reserved = ticket(); reserved.reservedDays = 15; kind("review", classify(reserved), "ticket with reserved duration"); } private static void specialAcquisitionRequiresReview() { RepairSkuDuration.Ticket vip = ticket(); vip.vip = true; kind("review", classify(vip), "VIP ticket"); RepairSkuDuration.Ticket trial = ticket(); trial.aqType = 2; kind("review", classify(trial), "trial acquisition"); } private static void generatedSqlMatchesTheOldSnapshot() { RepairSkuDuration.Ticket ticket = ticket(); String sql = normalizeSql(RepairSkuDuration.updateSql(ticket)); containsPattern(sql, "\\bupdate\\s+groups_relation\\b", "SQL updates the ticket table"); snapshotNumber(sql, "id", ticket.id); snapshotNumber(sql, "groups_id", ticket.groupsId); snapshotNumber(sql, "user_id", ticket.userId); snapshotNumber(sql, "sku_id", ticket.skuId); snapshotDate(sql, "start_time", "2026-01-15 10:20:30"); snapshotDate(sql, "expiry_time", "2027-01-15 10:20:30"); snapshotDate(sql, "update_time", "2026-01-15 10:21:40"); containsPattern(sql, "\\bwhere\\b", "SQL contains a guarded WHERE clause"); } private static void nullableUpdatedSnapshotIsPreserved() { RepairSkuDuration.Ticket ticket = ticket(); ticket.updated = null; String sql = normalizeSql(RepairSkuDuration.updateSql(ticket)); containsPattern(sql, "\\bupdate_time\\s*(?:is\\s+null|<=>\\s*null)\\b", "null update timestamp is compared as null"); } private static RepairSkuDuration.Ticket ticket() { RepairSkuDuration.Ticket ticket = new RepairSkuDuration.Ticket(); ticket.id = 880011L; ticket.groupsId = 660022L; ticket.skuId = 772L; ticket.userId = 440033L; ticket.status = "validity"; ticket.start = time("2026-01-15T10:20:30"); ticket.expiry = time("2027-01-15T10:20:30"); ticket.updated = time("2026-01-15T10:21:40"); ticket.aqType = 1; return ticket; } private static RepairSkuDuration.Purchase purchase(RepairSkuDuration.Ticket ticket) { RepairSkuDuration.Purchase purchase = new RepairSkuDuration.Purchase(); purchase.id = 330044L; purchase.skuId = ticket.skuId; purchase.orderType = 1; purchase.quantity = 1; return purchase; } private static RepairSkuDuration.Decision classify(RepairSkuDuration.Ticket ticket) { return classify(ticket, Collections.singletonList(purchase(ticket)), 0); } private static RepairSkuDuration.Decision classify(RepairSkuDuration.Ticket ticket, List purchases, int adjustmentCount) { return RepairSkuDuration.classify(ticket, purchases, adjustmentCount); } private static LocalDateTime time(String value) { return LocalDateTime.parse(value); } private static void kind(String expected, RepairSkuDuration.Decision decision, String description) { equal(expected, decision.kind, description + "; reason=" + decision.reason); } private static void equal(Object expected, Object actual, String description) { checks++; if (expected == null ? actual != null : !expected.equals(actual)) { throw new AssertionError(description + ": expected=" + expected + ", actual=" + actual); } } private static String normalizeSql(String sql) { return sql.replace("`", "").replaceAll("\\s+", " "); } private static void snapshotNumber(String sql, String column, long value) { containsPattern(sql, "\\b" + column + "\\s*(?:=|<=>)\\s*" + value + "\\b", "SQL guards the original " + column); } private static void snapshotDate(String sql, String column, String value) { containsPattern(sql, "\\b" + column + "\\s*(?:=|<=>)\\s*'" + Pattern.quote(value) + "(?:\\.0+)?'", "SQL guards the original " + column); } private static void containsPattern(String value, String regex, String description) { checks++; if (!Pattern.compile(regex, Pattern.CASE_INSENSITIVE).matcher(value).find()) { throw new AssertionError(description + ": expected pattern=" + regex + ", SQL=" + value); } } }