diff --git a/src/main/resources/db/migration/V20260831_1200__IS_repair_ask_chat_initial_free_turn_backfill.sql b/src/main/resources/db/migration/V20260831_1200__IS_repair_ask_chat_initial_free_turn_backfill.sql new file mode 100644 index 00000000..f1beea06 --- /dev/null +++ b/src/main/resources/db/migration/V20260831_1200__IS_repair_ask_chat_initial_free_turn_backfill.sql @@ -0,0 +1,66 @@ +WITH target_users AS ( + SELECT u.id AS user_id + FROM users u + WHERE NOT EXISTS ( + SELECT 1 + FROM ask_chat_wallet_logs l + WHERE l.user_id = u.id + AND l.reason = 'INITIAL_FREE_GRANT' + ) +), +granted_wallets AS ( + INSERT INTO ask_chat_wallets ( + user_id, + free_turn_balance, + paid_turn_balance, + version, + created_at, + updated_at + ) + SELECT + user_id, + 3, + 0, + 0, + NOW(), + NOW() + FROM target_users + ON CONFLICT (user_id) DO UPDATE + SET free_turn_balance = ask_chat_wallets.free_turn_balance + EXCLUDED.free_turn_balance, + updated_at = NOW(), + version = ask_chat_wallets.version + 1 + RETURNING + user_id, + free_turn_balance, + paid_turn_balance +) +INSERT INTO ask_chat_wallet_logs ( + user_id, + session_id, + message_id, + free_turn_delta, + paid_turn_delta, + free_turn_balance_after, + paid_turn_balance_after, + reason, + status, + ref_type, + ref_id, + idempotency_key, + created_at +) +SELECT + user_id, + NULL, + NULL, + 3, + 0, + free_turn_balance, + paid_turn_balance, + 'INITIAL_FREE_GRANT', + 'CONFIRMED', + 'SIGNUP', + user_id, + 'ask-chat-initial-free-' || user_id, + NOW() +FROM granted_wallets; diff --git a/src/test/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatInitialFreeTurnBackfillMigrationTest.java b/src/test/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatInitialFreeTurnBackfillMigrationTest.java new file mode 100644 index 00000000..b4fd33c4 --- /dev/null +++ b/src/test/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatInitialFreeTurnBackfillMigrationTest.java @@ -0,0 +1,99 @@ +package com.devkor.ifive.nadab.domain.askchat.core.repository; + +import com.devkor.ifive.nadab.domain.askchat.core.entity.AskChatWallet; +import com.devkor.ifive.nadab.domain.askchat.core.entity.AskChatWalletLog; +import com.devkor.ifive.nadab.domain.askchat.core.entity.AskChatWalletLogReason; +import com.devkor.ifive.nadab.domain.user.core.entity.User; +import com.devkor.ifive.nadab.infra.builder.UserBuilder; +import com.devkor.ifive.nadab.infra.db.PostgresIntegrationTestSupport; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ActiveProfiles("test") +class AskChatInitialFreeTurnBackfillMigrationTest extends PostgresIntegrationTestSupport { + + @Autowired + AskChatWalletRepository askChatWalletRepository; + + @Autowired + AskChatWalletLogRepository askChatWalletLogRepository; + + @Autowired + TestEntityManager em; + + @Autowired + JdbcTemplate jdbcTemplate; + + @Value("classpath:db/migration/V20260831_1200__IS_repair_ask_chat_initial_free_turn_backfill.sql") + Resource migration; + + @Test + void backfill_grants_missing_initial_turns_once_and_preserves_existing_balances() throws IOException { + User userWithoutWallet = new UserBuilder(em).build(); + + User userWithWallet = new UserBuilder(em).build(); + askChatWalletRepository.save(AskChatWallet.create(userWithWallet, 0, 7)); + + User alreadyGrantedUser = new UserBuilder(em).build(); + askChatWalletRepository.save(AskChatWallet.create(alreadyGrantedUser, 1, 5)); + askChatWalletLogRepository.save(AskChatWalletLog.createConfirmed( + alreadyGrantedUser, + null, + null, + 3, + 0, + 3, + 0, + AskChatWalletLogReason.INITIAL_FREE_GRANT, + "SIGNUP", + alreadyGrantedUser.getId(), + "ask-chat-initial-free-" + alreadyGrantedUser.getId() + )); + em.flush(); + + String migrationSql = migration.getContentAsString(StandardCharsets.UTF_8); + jdbcTemplate.execute(migrationSql); + jdbcTemplate.execute(migrationSql); + em.clear(); + + assertWalletBalances(userWithoutWallet.getId(), 3, 0); + assertWalletBalances(userWithWallet.getId(), 3, 7); + assertWalletBalances(alreadyGrantedUser.getId(), 1, 5); + assertInitialGrantLog(userWithoutWallet.getId(), 3, 0); + assertInitialGrantLog(userWithWallet.getId(), 3, 7); + assertInitialGrantLog(alreadyGrantedUser.getId(), 3, 0); + } + + private void assertWalletBalances(Long userId, int expectedFreeTurns, int expectedPaidTurns) { + AskChatWallet wallet = askChatWalletRepository.findByUserId(userId).orElseThrow(); + assertThat(wallet.getFreeTurnBalance()).isEqualTo(expectedFreeTurns); + assertThat(wallet.getPaidTurnBalance()).isEqualTo(expectedPaidTurns); + } + + private void assertInitialGrantLog(Long userId, int expectedFreeBalance, int expectedPaidBalance) { + List initialGrantLogs = askChatWalletLogRepository + .findAllByUserIdOrderByCreatedAtDesc(userId).stream() + .filter(log -> log.getReason() == AskChatWalletLogReason.INITIAL_FREE_GRANT) + .toList(); + + assertThat(initialGrantLogs).hasSize(1); + AskChatWalletLog initialGrantLog = initialGrantLogs.getFirst(); + assertThat(initialGrantLog.getFreeTurnDelta()).isEqualTo(3); + assertThat(initialGrantLog.getPaidTurnDelta()).isZero(); + assertThat(initialGrantLog.getFreeTurnBalanceAfter()).isEqualTo(expectedFreeBalance); + assertThat(initialGrantLog.getPaidTurnBalanceAfter()).isEqualTo(expectedPaidBalance); + } +}