Skip to content

Commit a66f543

Browse files
committed
feat: add ForceERPSyncOnStart configuration and update cron job behavior based on ERP integration
1 parent 527a53f commit a66f543

File tree

5 files changed

+172
-9
lines changed

5 files changed

+172
-9
lines changed

cmd/app/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func main() {
3333
gocontainer.Build(&cfg)
3434

3535
// Start cron jobs
36-
stopCron, err := cron.RunCron()
36+
stopCron, err := cron.RunCron(&cfg)
3737
if err != nil {
3838
panic(fmt.Sprintf("Error starting cron jobs: %v\n", err))
3939
}

internal/config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type Config struct {
3030

3131
AutoRegistrationYearGroups []int32 `env:"AUTO_REGISTRATION_YEAR_GROUPS" envSeparator:","`
3232

33+
ForceERPSyncOnStart bool `env:"FORCE_ERP_SYNC_ON_START" envDefault:"false"`
34+
3335
erpLocation *time.Location
3436
appLocation *time.Location
3537
}
@@ -118,3 +120,7 @@ func (c *Config) ERPTimeLocation() *time.Location {
118120
func (c *Config) APPTimeLocation() *time.Location {
119121
return c.appLocation
120122
}
123+
124+
func (c *Config) IsERPIntegrated() bool {
125+
return c.ISAMSBaseURL != "" && c.ISAMSAPIClientID != "" && c.ISAMSAPIClientSecret != ""
126+
}

internal/config/config_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,41 @@ func TestNewFromEnv_AutoRegistrationYearGroups(t *testing.T) {
215215
assert.Equal(t, []int32{6, 7, 8}, cfg.AutoRegistrationYearGroups)
216216
}
217217

218+
func TestNewFromEnv_ForceERPSyncOnStart(t *testing.T) {
219+
tests := []struct {
220+
name string
221+
envValue string
222+
expected bool
223+
}{
224+
{
225+
name: "true value",
226+
envValue: "true",
227+
expected: true,
228+
},
229+
{
230+
name: "false value",
231+
envValue: "false",
232+
expected: false,
233+
},
234+
{
235+
name: "empty uses default false",
236+
envValue: "",
237+
expected: false,
238+
},
239+
}
240+
for _, tc := range tests {
241+
t.Run(tc.name, func(t *testing.T) {
242+
loadTestEnvVariables(t, map[string]string{
243+
"FORCE_ERP_SYNC_ON_START": tc.envValue,
244+
})
245+
246+
cfg, err := NewFromEnv()
247+
assert.NoError(t, err)
248+
assert.Equal(t, tc.expected, cfg.ForceERPSyncOnStart)
249+
})
250+
}
251+
}
252+
218253
func TestNewFromEnv_ParseError(t *testing.T) {
219254
oldParse := parseEnv
220255
parseEnv = func(_ interface{}, _ ...env.Options) error {
@@ -254,6 +289,57 @@ func TestGetDatabaseURLForMysqlFromEnv_ParseError(t *testing.T) {
254289
assert.False(t, ok)
255290
}
256291

292+
func TestIsERPIntegrated(t *testing.T) {
293+
var tests = []struct {
294+
name string
295+
newEnv map[string]string
296+
expectedResult bool
297+
}{
298+
{
299+
name: "all required variables are set",
300+
newEnv: map[string]string{
301+
"ISAMS_BASE_URL": "https://example.com",
302+
"ISAMS_API_CLIENT_ID": "client-id",
303+
"ISAMS_API_CLIENT_SECRET": "client-secret",
304+
},
305+
expectedResult: true,
306+
},
307+
{
308+
name: "missing ISAMS_BASE_URL",
309+
newEnv: map[string]string{
310+
"ISAMS_API_CLIENT_ID": "client-id",
311+
"ISAMS_API_CLIENT_SECRET": "client-secret",
312+
},
313+
expectedResult: false,
314+
},
315+
{
316+
name: "missing ISAMS_API_CLIENT_ID",
317+
newEnv: map[string]string{
318+
"ISAMS_BASE_URL": "https://example.com",
319+
"ISAMS_API_CLIENT_SECRET": "client-secret",
320+
},
321+
expectedResult: false,
322+
},
323+
{
324+
name: "missing ISAMS_API_CLIENT_SECRET",
325+
newEnv: map[string]string{
326+
"ISAMS_BASE_URL": "https://example.com",
327+
"ISAMS_API_CLIENT_ID": "client-id",
328+
},
329+
expectedResult: false,
330+
},
331+
}
332+
for _, tc := range tests {
333+
t.Run(tc.name, func(t *testing.T) {
334+
loadTestEnvVariables(t, tc.newEnv)
335+
336+
cfg, err := NewFromEnv()
337+
assert.NoError(t, err)
338+
assert.Equal(t, tc.expectedResult, cfg.IsERPIntegrated())
339+
})
340+
}
341+
}
342+
257343
func loadTestEnvVariables(t *testing.T, env map[string]string) {
258344
t.Helper()
259345
resetConfigEnv(t)
@@ -281,6 +367,7 @@ func resetConfigEnv(t *testing.T) {
281367
"STUDENTS_IMAGE_PHOTO_DIR",
282368
"STUDENTS_IMAGE_PHOTO_URL_PREFIX",
283369
"AUTO_REGISTRATION_YEAR_GROUPS",
370+
"FORCE_ERP_SYNC_ON_START",
284371
"DATABASE_HOST",
285372
"DATABASE_PORT",
286373
"DATABASE_USERNAME",

internal/infrastructure/cron/cron_manager.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ type JobFunction func()
1616

1717
type StopCronFunc = func()
1818

19-
func RunCron() (StopCronFunc, error) {
19+
func RunCron(cfg *config.Config) (StopCronFunc, error) {
20+
if !cfg.IsERPIntegrated() {
21+
return func() {}, nil
22+
}
23+
2024
s, err := gocron.NewScheduler()
2125
if err != nil {
2226
return nil, err
2327
}
2428

25-
err = registerJobs(s)
29+
err = registerJobs(s, cfg.ForceERPSyncOnStart)
2630
if err != nil {
2731
return nil, err
2832
}
@@ -34,7 +38,7 @@ func RunCron() (StopCronFunc, error) {
3438
}, nil
3539
}
3640

37-
func registerJobs(s gocron.Scheduler) error {
41+
func registerJobs(s gocron.Scheduler, forceERPSyncOnStart bool) error {
3842
// Student data sync job
3943
_, err := s.NewJob(
4044
gocron.CronJob(
@@ -87,11 +91,13 @@ func registerJobs(s gocron.Scheduler) error {
8791
// You can register more cron jobs here
8892
// ...
8993

90-
// one-time startup run, sequential
91-
go func() {
92-
StudentSyncFunc()()
93-
StudentPhotosSyncFunc()()
94-
}()
94+
// one-time startup run, sequential (only if forceERPSyncOnStart is enabled)
95+
if forceERPSyncOnStart {
96+
go func() {
97+
StudentSyncFunc()()
98+
StudentPhotosSyncFunc()()
99+
}()
100+
}
95101

96102
return nil
97103
}

internal/infrastructure/cron/cron_manager_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,67 @@ func TestMarkNotRegisteredStudentsAsAbsent_WithoutConfiguredYearGroups(t *testin
118118
assert.Equal(t, 1, repo.optsLen)
119119
assert.Empty(t, repo.yearGroups)
120120
}
121+
122+
func TestRunCron_WhenERPNotIntegrated_ReturnsNoopAndNoError(t *testing.T) {
123+
cfg := &config.Config{
124+
ISAMSBaseURL: "",
125+
ISAMSAPIClientID: "",
126+
ISAMSAPIClientSecret: "",
127+
}
128+
129+
stopFunc, err := RunCron(cfg)
130+
131+
assert.NoError(t, err)
132+
assert.NotNil(t, stopFunc)
133+
// Calling stop should not panic
134+
stopFunc()
135+
}
136+
137+
func TestRunCron_WhenERPIntegrated_ReturnsStopFuncAndNoError(t *testing.T) {
138+
cfg := &config.Config{
139+
ISAMSBaseURL: "https://example.com",
140+
ISAMSAPIClientID: "client-id",
141+
ISAMSAPIClientSecret: "client-secret",
142+
ForceERPSyncOnStart: false,
143+
}
144+
145+
stopFunc, err := RunCron(cfg)
146+
147+
assert.NoError(t, err)
148+
assert.NotNil(t, stopFunc)
149+
stopFunc()
150+
}
151+
152+
func TestRunCron_WhenERPIntegratedAndForceERPSyncOnStart_StartsWithoutError(t *testing.T) {
153+
testContainer := container.New()
154+
oldGlobal := container.Global
155+
container.Global = testContainer
156+
t.Cleanup(func() {
157+
container.Global = oldGlobal
158+
})
159+
160+
syncCalled := false
161+
photosCalled := false
162+
163+
// Setup minimal container dependencies
164+
container.MustSingleton(container.Global, func() *zap.SugaredLogger { return zap.NewNop().Sugar() })
165+
166+
cfg := &config.Config{
167+
ISAMSBaseURL: "https://example.com",
168+
ISAMSAPIClientID: "client-id",
169+
ISAMSAPIClientSecret: "client-secret",
170+
ForceERPSyncOnStart: true,
171+
}
172+
173+
// We can't easily test that goroutine runs without more complex setup
174+
// but we can at least verify the function doesn't panic
175+
stopFunc, err := RunCron(cfg)
176+
177+
assert.NoError(t, err)
178+
assert.NotNil(t, stopFunc)
179+
stopFunc()
180+
181+
// Verify the variables are unused (just to avoid linter warnings about unused variables)
182+
_ = syncCalled
183+
_ = photosCalled
184+
}

0 commit comments

Comments
 (0)