package services import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/repositories" "github.com/treytartt/casera-api/internal/testutil" ) // setupSubscriptionService creates a SubscriptionService with the given // IAP clients (nil means "not configured"). It bypasses NewSubscriptionService // which tries to load config from environment. func setupSubscriptionService(t *testing.T, appleClient *AppleIAPClient, googleClient *GoogleIAPClient) (*SubscriptionService, *repositories.SubscriptionRepository) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) // Create a test user and subscription record for the test user := testutil.CreateTestUser(t, db, "subuser", "subuser@test.com", "password") // Create subscription record so GetOrCreate will find it sub := &models.UserSubscription{ UserID: user.ID, Tier: models.TierFree, } err := db.Create(sub).Error require.NoError(t, err) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: appleClient, googleClient: googleClient, } return svc, subscriptionRepo } func TestProcessApplePurchase_ClientNil_ReturnsError(t *testing.T) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) user := testutil.CreateTestUser(t, db, "subuser", "subuser@test.com", "password") sub := &models.UserSubscription{UserID: user.ID, Tier: models.TierFree} require.NoError(t, db.Create(sub).Error) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: nil, // Not configured googleClient: nil, } _, err := svc.ProcessApplePurchase(user.ID, "fake-receipt", "") assert.Error(t, err, "ProcessApplePurchase should return error when Apple IAP client is nil") // Verify user was NOT upgraded to Pro updatedSub, err := subscriptionRepo.GetOrCreate(user.ID) require.NoError(t, err) assert.Equal(t, models.TierFree, updatedSub.Tier, "User should remain on free tier when IAP client is nil") } func TestProcessApplePurchase_ValidationFails_DoesNotUpgrade(t *testing.T) { // We cannot easily create a real AppleIAPClient that will fail validation // in a unit test (it requires real keys and network access). // Instead, we test the code path logic: // When appleClient is nil, the service must NOT upgrade the user. // This is the same as TestProcessApplePurchase_ClientNil_ReturnsError // but validates no fallback occurs for the specific case. db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) user := testutil.CreateTestUser(t, db, "subuser2", "subuser2@test.com", "password") sub := &models.UserSubscription{UserID: user.ID, Tier: models.TierFree} require.NoError(t, db.Create(sub).Error) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: nil, googleClient: nil, } // Neither receipt data nor transaction ID - should still not grant Pro _, err := svc.ProcessApplePurchase(user.ID, "", "") assert.Error(t, err, "ProcessApplePurchase should return error when client is nil, even with empty data") // Verify no upgrade happened updatedSub, err := subscriptionRepo.GetOrCreate(user.ID) require.NoError(t, err) assert.Equal(t, models.TierFree, updatedSub.Tier, "User should remain on free tier") } func TestProcessGooglePurchase_ClientNil_ReturnsError(t *testing.T) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) user := testutil.CreateTestUser(t, db, "subuser3", "subuser3@test.com", "password") sub := &models.UserSubscription{UserID: user.ID, Tier: models.TierFree} require.NoError(t, db.Create(sub).Error) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: nil, googleClient: nil, // Not configured } _, err := svc.ProcessGooglePurchase(user.ID, "fake-token", "com.tt.casera.pro.monthly") assert.Error(t, err, "ProcessGooglePurchase should return error when Google IAP client is nil") // Verify user was NOT upgraded to Pro updatedSub, err := subscriptionRepo.GetOrCreate(user.ID) require.NoError(t, err) assert.Equal(t, models.TierFree, updatedSub.Tier, "User should remain on free tier when IAP client is nil") } func TestProcessGooglePurchase_ValidationFails_DoesNotUpgrade(t *testing.T) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) user := testutil.CreateTestUser(t, db, "subuser4", "subuser4@test.com", "password") sub := &models.UserSubscription{UserID: user.ID, Tier: models.TierFree} require.NoError(t, db.Create(sub).Error) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, appleClient: nil, googleClient: nil, // Not configured } // With empty token _, err := svc.ProcessGooglePurchase(user.ID, "", "") assert.Error(t, err, "ProcessGooglePurchase should return error when client is nil") // Verify no upgrade happened updatedSub, err := subscriptionRepo.GetOrCreate(user.ID) require.NoError(t, err) assert.Equal(t, models.TierFree, updatedSub.Tier, "User should remain on free tier") } func TestIsAlreadyProFromOtherPlatform(t *testing.T) { future := time.Now().UTC().Add(30 * 24 * time.Hour) tests := []struct { name string tier models.SubscriptionTier platform string expiresAt *time.Time trialEnd *time.Time requestedPlatform string wantConflict bool wantPlatform string }{ { name: "free user returns no conflict", tier: models.TierFree, platform: "", expiresAt: nil, trialEnd: nil, requestedPlatform: "stripe", wantConflict: false, wantPlatform: "", }, { name: "pro from ios, requesting ios returns no conflict (same platform)", tier: models.TierPro, platform: "ios", expiresAt: &future, trialEnd: nil, requestedPlatform: "ios", wantConflict: false, wantPlatform: "", }, { name: "pro from ios, requesting stripe returns conflict", tier: models.TierPro, platform: "ios", expiresAt: &future, trialEnd: nil, requestedPlatform: "stripe", wantConflict: true, wantPlatform: "ios", }, { name: "pro from stripe, requesting android returns conflict", tier: models.TierPro, platform: "stripe", expiresAt: &future, trialEnd: nil, requestedPlatform: "android", wantConflict: true, wantPlatform: "stripe", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) svc := &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, } user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") sub := &models.UserSubscription{ UserID: user.ID, Tier: tt.tier, Platform: tt.platform, ExpiresAt: tt.expiresAt, TrialEnd: tt.trialEnd, } err := db.Create(sub).Error require.NoError(t, err) conflict, existingPlatform, err := svc.IsAlreadyProFromOtherPlatform(user.ID, tt.requestedPlatform) require.NoError(t, err) assert.Equal(t, tt.wantConflict, conflict) assert.Equal(t, tt.wantPlatform, existingPlatform) }) } }