-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathenforce.go
247 lines (214 loc) · 7.15 KB
/
enforce.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
package job
import (
"context"
"fmt"
"strings"
"time"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"flag"
"github.com/weaveworks/common/instrument"
"github.com/weaveworks/common/logging"
"github.com/weaveworks/common/user"
"github.com/weaveworks/service/billing-api/trial"
"github.com/weaveworks/service/common/orgs"
"github.com/weaveworks/service/users"
)
var trialNotifiedCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "billing",
Subsystem: "enforcer",
Name: "trial_notified",
Help: "Number of organizations notified",
}, []string{"type", "status"})
func init() {
prometheus.MustRegister(trialNotifiedCount)
}
// Config provides settings for this job.
type Config struct {
NotifyPendingExpiryPeriod time.Duration
}
// RegisterFlags registers configuration variables.
func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
f.DurationVar(&cfg.NotifyPendingExpiryPeriod,
"trial.notify-pending-expiry-period", 3*24*time.Hour,
"Duration before trial expiry when we send the notification")
}
// Enforce job sends notification emails.
type Enforce struct {
users users.UsersClient
cfg Config
collector *instrument.JobCollector
}
// NewEnforce instantiates Enforce.
func NewEnforce(client users.UsersClient, cfg Config, collector *instrument.JobCollector) *Enforce {
return &Enforce{
users: client,
cfg: cfg,
collector: collector,
}
}
// Run starts the job and logs errors.
func (j *Enforce) Run() {
if err := j.Do(); err != nil {
log.Errorf("Error running job: %v", err)
}
}
// Do starts the job and returns an error if it fails.
func (j *Enforce) Do() error {
return instrument.CollectedRequest(context.Background(), "Enforce.Do", j.collector, nil, func(ctx context.Context) error {
now := time.Now().UTC()
var errs []string
for _, call := range []func(context.Context, time.Time) error{
j.NotifyTrialOrganizations,
j.ProcessDelinquentOrganizations,
} {
if err := call(ctx, now); err != nil {
errs = append(errs, err.Error())
}
}
if len(errs) > 0 {
return fmt.Errorf("%v", strings.Join(errs, " / "))
}
return nil
})
}
// NotifyTrialOrganizations sends emails to organizations for pending trial expiry.
func (j *Enforce) NotifyTrialOrganizations(ctx context.Context, now time.Time) error {
logger := user.LogWith(ctx, logging.Global())
resp, err := j.users.GetTrialOrganizations(ctx, &users.GetTrialOrganizationsRequest{
Now: now,
})
if err != nil {
return errors.Wrap(err, "failed getting trial organizations")
}
logger.Infof("Received %d trial organizations", len(resp.Organizations))
ok := 0
fail := 0
for _, org := range resp.Organizations {
// TODO: move filters to GetTrialOrganizationsRequest
if !org.IsOnboarded() {
continue
}
// Have we already notified?
if org.TrialPendingExpiryNotifiedAt != nil {
continue
}
// Are we within notification range of expiration?
expiresIn := org.TrialExpiresAt.Sub(now)
if expiresIn > j.cfg.NotifyPendingExpiryPeriod {
continue
}
// Trial already expired
if expiresIn <= 0 {
continue
}
// No payment method added?
if org.ZuoraAccountNumber != "" {
continue
}
_, err := j.users.NotifyTrialPendingExpiry(ctx, &users.NotifyTrialPendingExpiryRequest{
ExternalID: org.ExternalID,
})
if err == nil {
logger.Infof("Notified trial pending expiry for organization: %s", org.ExternalID)
ok++
} else {
logger.Errorf("Failed notifying trial organization %s: %v", org.ExternalID, err)
fail++
}
}
trialNotifiedCount.WithLabelValues("trial_pending_expiry", "success").Set(float64(ok))
trialNotifiedCount.WithLabelValues("trial_pending_expiry", "error").Set(float64(fail))
return nil
}
// ProcessDelinquentOrganizations sends emails to organizations whose trial has expired.
// It also makes sure their RefuseDataAccess flag is set and if delinquent for more than
// 15 days, RefuseDataUpload is set.
func (j *Enforce) ProcessDelinquentOrganizations(ctx context.Context, now time.Time) error {
logger := user.LogWith(ctx, logging.Global())
resp, err := j.users.GetDelinquentOrganizations(ctx, &users.GetDelinquentOrganizationsRequest{
Now: now,
})
if err != nil {
return errors.Wrap(err, "failed getting delinquent organizations")
}
logger.Infof("Received %d delinquent organizations", len(resp.Organizations))
ok := 0
fail := 0
for _, org := range resp.Organizations {
// TODO: move filters to GetDelinquentOrganizationsRequest
if !org.IsOnboarded() {
continue
}
// Failure in any of these flag updates should not interfere with the notification
// email. It will just pick it up on the next run. We do log an error.
j.refuseDataAccess(ctx, org, now)
// Only send notification if the instance actually had any trial and the RefuseDataUpload flag
// hasn't been set already. If there was no trial, they already received the `trial_expired` email today.
// No need to flood.
if j.refuseDataUpload(ctx, org, now) && trial.Length(org.TrialExpiresAt, org.CreatedAt) > 0 {
if _, err := j.users.NotifyRefuseDataUpload(ctx, &users.NotifyRefuseDataUploadRequest{ExternalID: org.ExternalID}); err == nil {
logger.Infof("Notified data upload refusal for organization: %s", org.ExternalID)
} else {
logger.Errorf("Failed notifying data upload refusal for organization %s: %v", org.ExternalID, err)
}
}
// Have we already notified?
if org.TrialExpiredNotifiedAt != nil {
continue
}
_, err := j.users.NotifyTrialExpired(ctx, &users.NotifyTrialExpiredRequest{
ExternalID: org.ExternalID,
})
if err == nil {
logger.Infof("Notified trial expired for organization: %s", org.ExternalID)
ok++
} else {
logger.Errorf("Failed notifying delinquent organization %s: %v", org.ExternalID, err)
fail++
}
}
trialNotifiedCount.WithLabelValues("trial_expired", "success").Set(float64(ok))
trialNotifiedCount.WithLabelValues("trial_expired", "error").Set(float64(fail))
return nil
}
func (j *Enforce) refuseDataAccess(ctx context.Context, org users.Organization, now time.Time) {
if org.RefuseDataAccess {
return
}
if !orgs.ShouldRefuseDataAccess(org, now) {
return
}
_, err := j.users.SetOrganizationFlag(ctx, &users.SetOrganizationFlagRequest{
ExternalID: org.ExternalID,
Flag: orgs.RefuseDataAccess,
Value: true,
})
logger := user.LogWith(ctx, logging.Global())
if err == nil {
logger.Infof("Refused data access for organization: %s", org.ExternalID)
} else {
logger.Errorf("Failed refusing data access for organization %s: %v", org.ExternalID, err)
}
}
func (j *Enforce) refuseDataUpload(ctx context.Context, org users.Organization, now time.Time) bool {
if org.RefuseDataUpload {
return false
}
if !orgs.ShouldRefuseDataUpload(org, now) {
return false
}
_, err := j.users.SetOrganizationFlag(ctx, &users.SetOrganizationFlagRequest{
ExternalID: org.ExternalID,
Flag: orgs.RefuseDataUpload,
Value: true,
})
logger := user.LogWith(ctx, logging.Global())
if err == nil {
logger.Infof("Refused data upload for organization: %s", org.ExternalID)
} else {
logger.Errorf("Failed refusing data upload for organization %s: %v", org.ExternalID, err)
}
return true
}