From 183d1c948a4a226f7e0c3bcaf7ab98edd409acb3 Mon Sep 17 00:00:00 2001 From: Janis Saldabols Date: Fri, 28 Aug 2026 11:03:32 +0300 Subject: [PATCH 1/4] ILLDEV-493 Don't send email if no patron requests are found --- broker/scheduler/service/email_sender.go | 4 +++ broker/scheduler/service/email_sender_test.go | 36 ++++++++++++------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/broker/scheduler/service/email_sender.go b/broker/scheduler/service/email_sender.go index 02ed5cf5a..ea51c937d 100644 --- a/broker/scheduler/service/email_sender.go +++ b/broker/scheduler/service/email_sender.go @@ -119,6 +119,10 @@ func (s *EmailSenderService) generateAndEmailPullslip(ctx common.ExtendedContext ctx.Logger().Warn("email batch truncated: selector matched more records than the per-email limit", "matched", fullCount, "limit", MAX_RECORDS_PER_EMAIL) } + if len(prs) == 0 { + ctx.Logger().Warn("email batch empty: selector matched no records", "selector", event.EventData.BatchActionData.Selector) + return events.EventStatusSuccess, &events.EventResult{CommonEventData: events.CommonEventData{Note: "no patron requests matched the selector"}} + } // Optionally generate a pull-slip PDF and attach it. var pdfAttachment *email.PdfAttach diff --git a/broker/scheduler/service/email_sender_test.go b/broker/scheduler/service/email_sender_test.go index c35f50cfd..bd6e198d3 100644 --- a/broker/scheduler/service/email_sender_test.go +++ b/broker/scheduler/service/email_sender_test.go @@ -379,7 +379,7 @@ func TestGenerateAndEmailPullslip_ListPatronRequestsError(t *testing.T) { } func TestGenerateAndEmailPullslip_SMTPError(t *testing.T) { - prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{}} + prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{{ID: "pr-1"}}} mailer := &mockEmailService{err: errors.New("SMTP unavailable")} svc := newEmailSvc(prRepo, mailer, nil) status, result := svc.generateAndEmailPullslip(testCtx, validEmailEvent()) @@ -388,12 +388,22 @@ func TestGenerateAndEmailPullslip_SMTPError(t *testing.T) { assert.True(t, mailer.called) } -func TestGenerateAndEmailPullslip_Success(t *testing.T) { +func TestGenerateAndEmailPullslip_SuccessNoEmail(t *testing.T) { prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{}} mailer := &mockEmailService{} svc := newEmailSvc(prRepo, mailer, nil) status, result := svc.generateAndEmailPullslip(testCtx, validEmailEvent()) assert.Equal(t, events.EventStatusSuccess, status) + assert.Equal(t, "no patron requests matched the selector", result.Note) + assert.False(t, mailer.called) +} + +func TestGenerateAndEmailPullslip_Success(t *testing.T) { + prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{{ID: "pr-1"}}} + mailer := &mockEmailService{} + svc := newEmailSvc(prRepo, mailer, nil) + status, result := svc.generateAndEmailPullslip(testCtx, validEmailEvent()) + assert.Equal(t, events.EventStatusSuccess, status) assert.Nil(t, result) assert.True(t, mailer.called) @@ -430,12 +440,14 @@ func TestGenerateAndEmailPullslip_PerformsPlaceholderSubstitution(t *testing.T) } func TestGenerateAndEmailPullslip_HtmlTemplate(t *testing.T) { - prRepo := &mockEmailPrRepo{template: pr_db.Template{ - ID: "template-id", - Subject: pgtype.Text{String: "Subject", Valid: true}, - Body: "

Body

", - ContentType: "html", - }} + prRepo := &mockEmailPrRepo{ + listResult: []pr_db.PatronRequest{{ID: "pr-1"}}, + template: pr_db.Template{ + ID: "template-id", + Subject: pgtype.Text{String: "Subject", Valid: true}, + Body: "

Body

", + ContentType: "html", + }} mailer := &mockEmailService{} svc := newEmailSvc(prRepo, mailer, nil) @@ -447,7 +459,7 @@ func TestGenerateAndEmailPullslip_HtmlTemplate(t *testing.T) { } func TestGenerateAndEmailPullslip_WithPDF_Success(t *testing.T) { - prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{}} + prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{{ID: "pr-1"}}} mailer := &mockEmailService{} pdf := &mockPdfGen{data: []byte("%PDF fake")} svc := newEmailSvc(prRepo, mailer, pdf) @@ -463,7 +475,7 @@ func TestGenerateAndEmailPullslip_WithPDF_Success(t *testing.T) { } func TestGenerateAndEmailPullslip_WithPDF_NilGenerator(t *testing.T) { - prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{}} + prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{{ID: "pr-1"}}} mailer := &mockEmailService{} // pdf generator is nil — IncludePdf=true must return an error, not panic. svc := newEmailSvc(prRepo, mailer, nil) @@ -478,7 +490,7 @@ func TestGenerateAndEmailPullslip_WithPDF_NilGenerator(t *testing.T) { } func TestGenerateAndEmailPullslip_WithPDF_GenerateError(t *testing.T) { - prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{}} + prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{{ID: "pr-1"}}} mailer := &mockEmailService{} pdf := &mockPdfGen{err: errors.New("pdf engine failure")} svc := newEmailSvc(prRepo, mailer, pdf) @@ -497,7 +509,7 @@ func TestGenerateAndEmailPullslip_WithPDF_GenerateError(t *testing.T) { // --------------------------------------------------------------------------- func TestEmailPullslip_WhenReadyToSend_SendsEmail(t *testing.T) { - prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{}} + prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{{ID: "pr-1"}}} mailer := &mockEmailService{ready: true} svc := EmailSenderServiceWithClient(prRepo, &mockEmailIllRepo{fromEmail: "from@example.com"}, mailer, nil) From 7e0e5a61d136262848ee951fcb224973d2b7b389 Mon Sep 17 00:00:00 2001 From: Janis Saldabols Date: Fri, 28 Aug 2026 16:15:51 +0300 Subject: [PATCH 2/4] ILLDEV-493 Remove log record --- broker/scheduler/service/email_sender.go | 1 - 1 file changed, 1 deletion(-) diff --git a/broker/scheduler/service/email_sender.go b/broker/scheduler/service/email_sender.go index ea51c937d..ed82047f0 100644 --- a/broker/scheduler/service/email_sender.go +++ b/broker/scheduler/service/email_sender.go @@ -120,7 +120,6 @@ func (s *EmailSenderService) generateAndEmailPullslip(ctx common.ExtendedContext "matched", fullCount, "limit", MAX_RECORDS_PER_EMAIL) } if len(prs) == 0 { - ctx.Logger().Warn("email batch empty: selector matched no records", "selector", event.EventData.BatchActionData.Selector) return events.EventStatusSuccess, &events.EventResult{CommonEventData: events.CommonEventData{Note: "no patron requests matched the selector"}} } From 5d7d33b61ba55fea034c247568c5e9b8ada3c243 Mon Sep 17 00:00:00 2001 From: Janis Saldabols Date: Mon, 31 Aug 2026 09:25:07 +0300 Subject: [PATCH 3/4] ILLDEV-493 Fix tests after rebase --- broker/scheduler/service/email_sender_test.go | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/broker/scheduler/service/email_sender_test.go b/broker/scheduler/service/email_sender_test.go index bd6e198d3..8ec29bd1b 100644 --- a/broker/scheduler/service/email_sender_test.go +++ b/broker/scheduler/service/email_sender_test.go @@ -324,15 +324,18 @@ func TestGenerateAndEmailPullslip_TemplateEmptySubject(t *testing.T) { } func TestGenerateAndEmailPullslip_TemplateInvalidBody(t *testing.T) { - prRepo := &mockEmailPrRepo{template: pr_db.Template{ - ID: "template-id", - Subject: pgtype.Text{ - Valid: true, - String: "Subject", + prRepo := &mockEmailPrRepo{ + template: pr_db.Template{ + ID: "template-id", + Subject: pgtype.Text{ + Valid: true, + String: "Subject", + }, + Body: "Body {{.Invalid text", + ContentType: "text", }, - Body: "Body {{.Invalid text", - ContentType: "text", - }} + listResult: []pr_db.PatronRequest{{ID: "pr-1"}}, + } svc := newEmailSvc(prRepo, &mockEmailService{}, nil) status, result := svc.generateAndEmailPullslip(testCtx, validEmailEvent()) assert.Equal(t, events.EventStatusError, status) @@ -341,15 +344,18 @@ func TestGenerateAndEmailPullslip_TemplateInvalidBody(t *testing.T) { } func TestGenerateAndEmailPullslip_TemplateInvalidSubject(t *testing.T) { - prRepo := &mockEmailPrRepo{template: pr_db.Template{ - ID: "template-id", - Subject: pgtype.Text{ - Valid: true, - String: "Subject {{.Invalid text", + prRepo := &mockEmailPrRepo{ + template: pr_db.Template{ + ID: "template-id", + Subject: pgtype.Text{ + Valid: true, + String: "Subject {{.Invalid text", + }, + Body: "Body", + ContentType: "text", }, - Body: "Body", - ContentType: "text", - }} + listResult: []pr_db.PatronRequest{{ID: "pr-1"}}, + } svc := newEmailSvc(prRepo, &mockEmailService{}, nil) status, result := svc.generateAndEmailPullslip(testCtx, validEmailEvent()) assert.Equal(t, events.EventStatusError, status) From fb51ddb769a39d2707f151780db9e29ee82f673a Mon Sep 17 00:00:00 2001 From: Janis Saldabols Date: Mon, 31 Aug 2026 17:01:49 +0300 Subject: [PATCH 4/4] ILLDEV-493 Add option to send empty pull-slip emails --- broker/scheduler/service/email_sender.go | 10 +++++++--- broker/scheduler/service/email_sender_test.go | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/broker/scheduler/service/email_sender.go b/broker/scheduler/service/email_sender.go index ed82047f0..d3c03b087 100644 --- a/broker/scheduler/service/email_sender.go +++ b/broker/scheduler/service/email_sender.go @@ -24,6 +24,7 @@ type pullslipEmailData struct { To []string `json:"to"` TemplateLabel string `json:"templateLabel"` IncludePdf bool `json:"includePdf"` + SendEmpty bool `json:"sendEmpty"` } type EmailSenderService struct { @@ -119,13 +120,14 @@ func (s *EmailSenderService) generateAndEmailPullslip(ctx common.ExtendedContext ctx.Logger().Warn("email batch truncated: selector matched more records than the per-email limit", "matched", fullCount, "limit", MAX_RECORDS_PER_EMAIL) } - if len(prs) == 0 { + if len(prs) == 0 && !emailData.SendEmpty { return events.EventStatusSuccess, &events.EventResult{CommonEventData: events.CommonEventData{Note: "no patron requests matched the selector"}} } // Optionally generate a pull-slip PDF and attach it. var pdfAttachment *email.PdfAttach - if emailData.IncludePdf { + includePdf := emailData.IncludePdf && len(prs) > 0 + if includePdf { if s.pdf == nil { return events.NewErrorResult("pdf not configured", "no PDF generator is available on this service instance") } @@ -155,7 +157,7 @@ func (s *EmailSenderService) generateAndEmailPullslip(ctx common.ExtendedContext Subject: subject, Body: body, IsHTML: template.ContentType == string(proapi.Html), - IncludePdf: emailData.IncludePdf, + IncludePdf: includePdf, } raw, err := email.BuildRawMessage(*owner.CustomData.FromEmail, messageData, pdfAttachment) @@ -198,10 +200,12 @@ func extractEmailData(eventData events.EventData) (pullslipEmailData, error) { templateLabel, _ := eventData.CustomData["templateLabel"].(string) includePdf, _ := eventData.CustomData["includePdf"].(bool) + sendEmpty, _ := eventData.CustomData["sendEmpty"].(bool) return pullslipEmailData{ To: toAddrs, TemplateLabel: templateLabel, IncludePdf: includePdf, + SendEmpty: sendEmpty, }, nil } diff --git a/broker/scheduler/service/email_sender_test.go b/broker/scheduler/service/email_sender_test.go index 8ec29bd1b..057e84974 100644 --- a/broker/scheduler/service/email_sender_test.go +++ b/broker/scheduler/service/email_sender_test.go @@ -404,6 +404,26 @@ func TestGenerateAndEmailPullslip_SuccessNoEmail(t *testing.T) { assert.False(t, mailer.called) } +func TestGenerateAndEmailPullslip_SuccessEmailWithZero(t *testing.T) { + prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{}} + mailer := &mockEmailService{} + svc := newEmailSvc(prRepo, mailer, nil) + event := validEmailEvent() + event.EventData.CustomData["sendEmpty"] = true + status, result := svc.generateAndEmailPullslip(testCtx, event) + assert.Equal(t, events.EventStatusSuccess, status) + assert.Nil(t, result) + assert.True(t, mailer.called) + + assert.True(t, strings.Contains(string(mailer.data), "user@example.com")) + assert.Equal(t, pr_db.GetTemplateByPurposeAudienceLabelAndOwnerParams{ + Owner: "ISIL:OWNER", + Purpose: "email", + Label: "pullslips", + Audience: "staff", + }, prRepo.gotTemplate) +} + func TestGenerateAndEmailPullslip_Success(t *testing.T) { prRepo := &mockEmailPrRepo{listResult: []pr_db.PatronRequest{{ID: "pr-1"}}} mailer := &mockEmailService{}