package entitlementsvc import ( "context" "errors" "testing" "time" "galaxy/user/internal/domain/common" "galaxy/user/internal/domain/entitlement" "galaxy/user/internal/ports" "github.com/stretchr/testify/require" ) func TestReaderGetByUserIDPublishesExpiredRepairEvent(t *testing.T) { t.Parallel() userID := common.UserID("user-123") startsAt := time.Unix(1_775_240_000, 0).UTC() endsAt := startsAt.Add(24 * time.Hour) now := endsAt.Add(time.Hour) snapshotStore := &fakeSnapshotStore{ byUserID: map[common.UserID]entitlement.CurrentSnapshot{ userID: paidSnapshot( userID, entitlement.PlanCodePaidMonthly, startsAt, endsAt, common.Source("admin"), common.ReasonCode("manual_grant"), ), }, } historyStore := &fakeHistoryStore{ byUserID: map[common.UserID][]entitlement.PeriodRecord{ userID: { paidRecord( entitlement.EntitlementRecordID("entitlement-paid"), userID, entitlement.PlanCodePaidMonthly, startsAt, endsAt, common.Source("admin"), common.ReasonCode("manual_grant"), ), }, }, } lifecycleStore := &fakeLifecycleStore{ historyStore: historyStore, snapshotStore: snapshotStore, } publisher := &recordingEntitlementPublisher{} reader, err := NewReaderWithObservability(snapshotStore, lifecycleStore, fixedClock{now: now}, fixedIDGenerator{ recordID: entitlement.EntitlementRecordID("entitlement-free"), }, nil, nil, publisher) require.NoError(t, err) got, err := reader.GetByUserID(context.Background(), userID) require.NoError(t, err) require.Equal(t, entitlement.PlanCodeFree, got.PlanCode) require.Len(t, publisher.events, 1) require.Equal(t, ports.EntitlementChangedOperationExpiredRepaired, publisher.events[0].Operation) require.Equal(t, common.Source("entitlement_expiry_repair"), publisher.events[0].Source) } func TestGrantServiceExecutePublisherFailureDoesNotRollbackResult(t *testing.T) { t.Parallel() now := time.Unix(1_775_240_000, 0).UTC() userID := common.UserID("user-123") currentFreeStartsAt := now.Add(-24 * time.Hour) currentSnapshot := freeSnapshot(userID, currentFreeStartsAt, common.Source("auth_registration"), common.ReasonCode("initial_free_entitlement")) currentRecord := freeRecord(entitlement.EntitlementRecordID("entitlement-free"), userID, currentFreeStartsAt, common.Source("auth_registration"), common.ReasonCode("initial_free_entitlement")) lifecycleStore := &fakeLifecycleStore{} publisher := &recordingEntitlementPublisher{err: errors.New("publisher unavailable")} service, err := NewGrantServiceWithObservability( fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}}, &fakeHistoryStore{byUserID: map[common.UserID][]entitlement.PeriodRecord{userID: {currentRecord}}}, fakeEffectiveReader{byUserID: map[common.UserID]entitlement.CurrentSnapshot{userID: currentSnapshot}}, lifecycleStore, fixedClock{now: now}, fixedIDGenerator{recordID: entitlement.EntitlementRecordID("entitlement-paid")}, nil, nil, publisher, ) require.NoError(t, err) result, err := service.Execute(context.Background(), GrantInput{ UserID: userID.String(), PlanCode: string(entitlement.PlanCodePaidMonthly), Source: "admin", ReasonCode: "manual_grant", Actor: ActorInput{Type: "admin", ID: "admin-1"}, StartsAt: now.Format(time.RFC3339Nano), EndsAt: now.Add(30 * 24 * time.Hour).Format(time.RFC3339Nano), }) require.NoError(t, err) require.Equal(t, entitlement.PlanCodePaidMonthly, result.Entitlement.PlanCode) require.Len(t, publisher.events, 1) require.Equal(t, ports.EntitlementChangedOperationGranted, publisher.events[0].Operation) } type recordingEntitlementPublisher struct { err error events []ports.EntitlementChangedEvent } func (publisher *recordingEntitlementPublisher) PublishEntitlementChanged(_ context.Context, event ports.EntitlementChangedEvent) error { if err := event.Validate(); err != nil { return err } publisher.events = append(publisher.events, event) return publisher.err } var _ ports.EntitlementChangedPublisher = (*recordingEntitlementPublisher)(nil)