fix: keep growth analysis available per terminal

This commit is contained in:
2026-08-21 02:57:46 +03:30
parent 7951664d08
commit e6afaa7a10
11 changed files with 180 additions and 45 deletions

View File

@@ -90,11 +90,21 @@ func (r *Repository) CreateOrReuse(ctx context.Context, in CreateRecord, beforeI
}
}
if in.ManualRefresh && in.RefreshDay != nil {
var exists bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM growth_reports WHERE merchant_id=$1 AND manual_refresh=true AND tehran_refresh_day=$2)`, in.MerchantID, in.RefreshDay).Scan(&exists); err != nil {
return Record{}, false, fmt.Errorf("checking daily refresh: %w", err)
refreshLockKey := fmt.Sprintf("growth-refresh:%d:%s", in.MerchantID, in.TerminalID)
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, refreshLockKey); err != nil {
return Record{}, false, fmt.Errorf("locking growth report refresh: %w", err)
}
if exists {
var hasAISummary bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM growth_reports WHERE merchant_id=$1 AND terminal_source_key IS NOT DISTINCT FROM $2::text AND status='READY' AND ai_response IS NOT NULL)`, in.MerchantID, nilIfEmpty(in.TerminalID)).Scan(&hasAISummary); err != nil {
return Record{}, false, fmt.Errorf("checking prior AI summary: %w", err)
}
var refreshUsed bool
if hasAISummary {
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM growth_reports WHERE merchant_id=$1 AND terminal_source_key IS NOT DISTINCT FROM $2::text AND manual_refresh=true AND tehran_refresh_day=$3)`, in.MerchantID, nilIfEmpty(in.TerminalID), in.RefreshDay).Scan(&refreshUsed); err != nil {
return Record{}, false, fmt.Errorf("checking daily refresh: %w", err)
}
}
if refreshUsed {
return Record{}, false, ErrRefreshLimited
}
}
@@ -110,9 +120,6 @@ func (r *Repository) CreateOrReuse(ctx context.Context, in CreateRecord, beforeI
_, err = tx.Exec(ctx, `INSERT INTO growth_reports(id,merchant_id,terminal_id,terminal_source_key,fingerprint,revision,status,forecast_start_date,dataset_watermark,catalog_version,analytics_version,prompt_version,schema_version,model,manual_refresh,tehran_refresh_day)
VALUES($1,$2,$3,$4,$5,$6,'QUEUED',$7,$8,$9,$10,$11,$12,$13,$14,$15)`, in.ID, in.MerchantID, terminalDBID, nilIfEmpty(in.TerminalID), in.Fingerprint, revision, in.ForecastStartDate, in.DatasetWatermark, in.CatalogVersion, in.AnalyticsVersion, in.PromptVersion, in.SchemaVersion, in.Model, in.ManualRefresh, in.RefreshDay)
if err != nil {
if in.ManualRefresh && isUniqueViolation(err) {
return Record{}, false, ErrRefreshLimited
}
return Record{}, false, fmt.Errorf("creating growth report: %w", err)
}
if in.IdempotencyKey != "" {
@@ -134,8 +141,8 @@ func (r *Repository) Get(ctx context.Context, merchantID int64, id string) (Reco
return getWith(ctx, r.pool, merchantID, id)
}
func (r *Repository) Latest(ctx context.Context, merchantID int64, terminalID string) (Record, error) {
return scanRecord(r.pool.QueryRow(ctx, reportSelect+` WHERE merchant_id=$1 AND terminal_source_key IS NOT DISTINCT FROM $2::text AND status IN ('READY','PARTIAL') ORDER BY created_at DESC LIMIT 1`, merchantID, nilIfEmpty(terminalID)))
func (r *Repository) Latest(ctx context.Context, merchantID int64, terminalID, analyticsVersion string) (Record, error) {
return scanRecord(r.pool.QueryRow(ctx, reportSelect+` WHERE merchant_id=$1 AND terminal_source_key IS NOT DISTINCT FROM $2::text AND analytics_version=$3 AND status IN ('READY','PARTIAL') ORDER BY created_at DESC LIMIT 1`, merchantID, nilIfEmpty(terminalID), analyticsVersion))
}
func (r *Repository) Claim(ctx context.Context) (Record, error) {
@@ -256,18 +263,16 @@ func nilIfEmpty(value string) any {
return value
}
func isUniqueViolation(err error) bool {
type sqlState interface{ SQLState() string }
var state sqlState
return errors.As(err, &state) && state.SQLState() == "23505"
}
func (r *Repository) NextRefreshAt(ctx context.Context, merchantID int64, now time.Time) (time.Time, error) {
func (r *Repository) NextRefreshAt(ctx context.Context, merchantID int64, terminalID string, now time.Time) (time.Time, error) {
var hasAISummary bool
var last *time.Time
if err := r.pool.QueryRow(ctx, `SELECT max(created_at) FROM growth_reports WHERE merchant_id=$1 AND manual_refresh=true`, merchantID).Scan(&last); err != nil {
if err := r.pool.QueryRow(ctx, `SELECT
EXISTS(SELECT 1 FROM growth_reports WHERE merchant_id=$1 AND terminal_source_key IS NOT DISTINCT FROM $2::text AND status='READY' AND ai_response IS NOT NULL),
max(created_at) FILTER (WHERE manual_refresh=true)
FROM growth_reports WHERE merchant_id=$1 AND terminal_source_key IS NOT DISTINCT FROM $2::text`, merchantID, nilIfEmpty(terminalID)).Scan(&hasAISummary, &last); err != nil {
return time.Time{}, fmt.Errorf("loading refresh quota: %w", err)
}
if last == nil {
if !hasAISummary || last == nil {
return now.UTC(), nil
}
return NextTehranRefresh(*last), nil