Skip to content

Commit 76f59d0

Browse files
dmitarsGFilipovich
andauthored
OD-18978 billing scripts restructure (#1260)
* OD-18978 feat: chargebee service * OD-18978 feat: job configuration for chargebee * OD-18978 fix: move logic from configs to job; remove some technical comments * OD-18978 feat: add replicated settings as cayenne entity; add billing.users settings to allowed for replication * OD-18978 feat: add upload of billing.users settings value to chargebee * OD-18978 feat: remove billing.users property from chargebee; add all required payment metrics * OD-18978 feat: local mode to test data without upload * OD-18978 refactor: move processing of properties to separate classes * OD-18978 fix: errors catching * OD-18978 fix: audit writing in local mode * OD-19292 feat: ignore of addons, that are not attached to subscriptions * OD-18978 feat: upgrade properties storage to preferences, add new properties * OD-18978 fix: description of office item * OD-18978 feat: new property for chargebee * OD-18978 fix: total office payments query * OD-18978 feat: replace long values for payment amounts with bigdecimal * OD-18978 fix: quantity representation * OD-18978 fix: office payments queries * OD-18978 fix: office query * OD-18978 fix: temp count number of payments instead of amount * OD-18978 fix: name of credit count property * OD-18978 fix: get double value instead of bigdecimal from db queries * OD-18978 fix: format of audit record * OD-18978 fix: convert of db query result --------- Co-authored-by: George Filipovich <63780201+GFilipovich@users.noreply.github.com>
1 parent a63f44f commit 76f59d0

26 files changed

+819
-1
lines changed

server/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ dependencies {
137137
api 'com.nimbusds:nimbus-jose-jwt:8.9'
138138
implementation 'com.bugsnag:bugsnag:3.6.2'
139139

140+
implementation 'com.chargebee:chargebee-java:3.19.0'
141+
140142
testImplementation "org.apache.cayenne:cayenne-dbsync:$cayenneVersion"
141143
testImplementation 'org.mockito:mockito-core:2.18.3'
142144
testImplementation 'commons-dbcp:commons-dbcp:1.4'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright ish group pty ltd 2024.
3+
*
4+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
5+
*
6+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
7+
*/
8+
9+
package ish.oncourse.server.cayenne
10+
11+
import ish.oncourse.server.cayenne.glue._Settings
12+
13+
class Settings extends _Settings implements Queueable {
14+
@Override
15+
boolean isAsyncReplicationAllowed() {
16+
return false
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright ish group pty ltd 2024.
3+
*
4+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
5+
*
6+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
7+
*/
8+
9+
package ish.oncourse.server.services.chargebee;
10+
11+
import com.google.inject.Provides;
12+
import com.google.inject.Singleton;
13+
import io.bootique.ConfigModule;
14+
import io.bootique.config.ConfigurationFactory;
15+
import ish.oncourse.server.ICayenneService;
16+
import ish.oncourse.server.PreferenceController;
17+
18+
public class ChargebeeModule extends ConfigModule {
19+
20+
@Singleton
21+
@Provides
22+
public ChargebeeService createChargebeeService(ConfigurationFactory configFactory, ICayenneService cayenneService,
23+
PreferenceController preferenceController) {
24+
return configFactory.config(ChargebeeService.class, getConfigPrefix())
25+
.createChargebeeService(cayenneService, preferenceController);
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright ish group pty ltd 2024.
3+
*
4+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
5+
*
6+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
7+
*/
8+
9+
package ish.oncourse.server.services.chargebee
10+
11+
class ChargebeeQueryUtils {
12+
13+
public static final String TOTAL_CREDIT_PAYMENT_AMOUNT_QUERY_FORMAT = "SELECT SUM(p.amount) AS value" +
14+
" FROM %s p JOIN PaymentMethod pm on p.paymentMethodId = pm.id" +
15+
" WHERE pm.type = 2 " +
16+
" AND p.createdOn >= '%s'" +
17+
" AND p.createdOn < '%s'" +
18+
" AND p.status IN (3, 6)"
19+
20+
public static final String TOTAL_CREDIT_PAYMENT_COUNT_QUERY_FORMAT = "SELECT COUNT(*) AS value" +
21+
" FROM %s p JOIN PaymentMethod pm on p.paymentMethodId = pm.id" +
22+
" WHERE pm.type = 2 " +
23+
" AND p.createdOn >= '%s'" +
24+
" AND p.createdOn < '%s'" +
25+
" AND p.status IN (3, 6)"
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright ish group pty ltd 2024.
3+
*
4+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
5+
*
6+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
7+
*/
8+
9+
package ish.oncourse.server.services.chargebee
10+
11+
import com.google.inject.Inject
12+
import io.bootique.annotation.BQConfigProperty
13+
import ish.common.chargebee.ChargebeePropertyType
14+
import ish.oncourse.server.ICayenneService
15+
import ish.oncourse.server.PreferenceController
16+
import ish.oncourse.server.cayenne.Preference
17+
import org.apache.cayenne.query.ObjectSelect
18+
19+
class ChargebeeService {
20+
private Boolean localMode = null
21+
22+
23+
@BQConfigProperty
24+
void setLocalMode(Boolean localMode) {
25+
this.localMode = localMode
26+
}
27+
28+
Boolean getLocalMode() {
29+
return localMode
30+
}
31+
32+
33+
private ICayenneService cayenneService
34+
private PreferenceController preferenceController
35+
36+
37+
String getSubscriptionId(){
38+
return preferenceController.getChargebeeSubscriptionId()
39+
}
40+
41+
List<String> getAllowedAddons() {
42+
def addons = preferenceController.getChargebeeAllowedAddons()
43+
if(addons == null)
44+
return new ArrayList<String>()
45+
46+
return addons.split(ChargebeePropertyType.ADDONS_SEPARATOR)?.toList()
47+
}
48+
49+
String configOf(ChargebeePropertyType type) {
50+
def preference = ObjectSelect.query(Preference)
51+
.where(Preference.NAME.eq(type.getDbPropertyName()))
52+
.selectOne(cayenneService.newContext)
53+
54+
if(preference == null)
55+
throw new IllegalStateException("Attempt to upload $type property to chargebee, but config was not replicated for this college")
56+
57+
return preference.getValueString()
58+
}
59+
60+
61+
ChargebeeService createChargebeeService(ICayenneService cayenneService, PreferenceController preferenceController) {
62+
this.cayenneService = cayenneService
63+
this.preferenceController = preferenceController
64+
this
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright ish group pty ltd 2024.
3+
*
4+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
5+
*
6+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
7+
*/
8+
9+
package ish.oncourse.server.services.chargebee
10+
11+
import com.chargebee.Environment
12+
import com.chargebee.models.Subscription
13+
import com.chargebee.models.Usage
14+
import com.google.inject.Inject
15+
import ish.common.chargebee.ChargebeePropertyType
16+
import ish.oncourse.server.ICayenneService
17+
import ish.oncourse.server.PreferenceController
18+
import ish.oncourse.server.cayenne.Script
19+
import ish.oncourse.server.messaging.MessageService
20+
import ish.oncourse.server.scripting.api.EmailService
21+
import ish.oncourse.server.scripting.api.EmailSpec
22+
import ish.oncourse.server.scripting.api.MessageSpec
23+
import ish.oncourse.server.services.AuditService
24+
import ish.oncourse.server.services.chargebee.property.ChargebeePropertyProcessor
25+
import ish.oncourse.server.services.chargebee.property.ChargeebeeProcessorFactory
26+
import ish.oncourse.types.AuditAction
27+
import org.apache.cayenne.query.ObjectSelect
28+
import org.apache.logging.log4j.LogManager
29+
import org.apache.logging.log4j.Logger
30+
import org.quartz.DisallowConcurrentExecution
31+
import org.quartz.Job
32+
import org.quartz.JobExecutionContext
33+
import org.quartz.JobExecutionException
34+
35+
import java.sql.Timestamp
36+
import java.time.Instant
37+
38+
@DisallowConcurrentExecution
39+
class ChargebeeUploadJob implements Job {
40+
private static final Logger logger = LogManager.getLogger()
41+
42+
@Inject
43+
private ICayenneService cayenneService
44+
45+
@Inject
46+
private ChargebeeService chargebeeService
47+
48+
@Inject
49+
private MessageService messageService
50+
51+
@Inject
52+
private PreferenceController preferenceController
53+
54+
@Inject
55+
private AuditService auditService
56+
57+
private static Subscription subscription = null
58+
59+
60+
@Override
61+
void execute(JobExecutionContext context) throws JobExecutionException {
62+
logger.warn("ChargebeeUploadJob started")
63+
64+
def addons = chargebeeService.getAllowedAddons()
65+
if(addons.isEmpty()) {
66+
logger.warn("ChargebeeUploadJob is rejected due to allowed addons not configured for this college")
67+
return
68+
}
69+
70+
def site = chargebeeService.configOf(ChargebeePropertyType.SITE)
71+
def apiKey = chargebeeService.configOf(ChargebeePropertyType.API_KEY)
72+
73+
74+
if (site == null || apiKey == null) {
75+
String error = "Try to use chargebee, but its configs don't have necessary field (site or api key)"
76+
logger.error(error)
77+
throw new RuntimeException(error)
78+
}
79+
80+
Calendar aCalendar = Calendar.getInstance()
81+
aCalendar.add(Calendar.MONTH, -1)
82+
aCalendar.set(Calendar.DATE, 1)
83+
def firstDateOfPreviousMonth = aCalendar.getTime()
84+
85+
aCalendar.add(Calendar.MONTH, 1)
86+
aCalendar.set(Calendar.DATE, 1)
87+
def firstDateOfCurrentMonth = aCalendar.getTime()
88+
89+
def propertiesToUpload = ChargebeePropertyType.getItems()
90+
.findAll {addons.contains(it.getDbPropertyName())}
91+
92+
logger.warn("Chargebee start date including $firstDateOfPreviousMonth , end date $firstDateOfCurrentMonth")
93+
94+
try {
95+
if(!chargebeeService.localMode)
96+
Environment.configure(site, apiKey)
97+
98+
propertiesToUpload.each { type ->
99+
def property = ChargeebeeProcessorFactory.valueOf(type, firstDateOfPreviousMonth, firstDateOfCurrentMonth)
100+
uploadUsageToSite(property)
101+
}
102+
} catch (Exception e) {
103+
logger.catching(e)
104+
throw e
105+
}
106+
107+
logger.warn("ChargeebeeUploadJob executed successfully")
108+
}
109+
110+
111+
private void uploadUsageToSite(ChargebeePropertyProcessor propertyProcessor) {
112+
if(propertyProcessor.type == null) {
113+
throw new IllegalArgumentException("Try to upload chargebee usage without item type")
114+
}
115+
116+
String itemPriceId = chargebeeService.configOf(propertyProcessor.type)
117+
if(itemPriceId == null)
118+
throw new IllegalArgumentException("Try to upload usage $propertyProcessor.type without configured item id")
119+
120+
def quantity = propertyProcessor.getValue(cayenneService.dataSource)
121+
logger.warn("Try to upload to chargebee $propertyProcessor.type with id $itemPriceId value $quantity")
122+
123+
if(Boolean.TRUE == chargebeeService.localMode)
124+
auditService.submit(ObjectSelect.query(Script).selectFirst(cayenneService.newReadonlyContext), AuditAction.SCRIPT_EXECUTED, "Try to upload to chargebee $propertyProcessor.type with id $itemPriceId value " + quantity.toPlainString())
125+
else {
126+
def subscription = getSubscription()
127+
if(!subscription.subscriptionItems().find {it.itemPriceId() == itemPriceId}) {
128+
logger.warn("Item price id $itemPriceId not allowed for subscription $chargebeeService.subscriptionId and will be ignored")
129+
return
130+
}
131+
uploadToChargebee(itemPriceId, quantity.toPlainString())
132+
}
133+
}
134+
135+
private void uploadToChargebee(String itemPriceId, String quantity) {
136+
try {
137+
Usage.create(chargebeeService.subscriptionId)
138+
.itemPriceId(itemPriceId)
139+
.quantity(quantity)
140+
.usageDate(new Timestamp(Instant.now().toEpochMilli()))
141+
.request()
142+
} catch (Exception e) {
143+
logger.error("Chargebee usage upload error: " + e.getMessage())
144+
messageService.sendMessage(new MessageSpec().with {
145+
it.subject = 'onCourse->Chargebee usage upload error. Contact ish support'
146+
it.content ="\n$itemPriceId upload error for college $preferenceController.collegeName. Reason: $e.message"
147+
it.from(preferenceController.emailFromAddress)
148+
it.to("accounts@ish.com.au")
149+
it
150+
})
151+
}
152+
}
153+
154+
private Subscription getSubscription(){
155+
if(subscription != null)
156+
return subscription
157+
158+
159+
def subscriptions = Subscription.list().id().is(chargebeeService.subscriptionId).request()
160+
if(subscriptions.empty) {
161+
throw new IllegalArgumentException("Subscription with id $chargebeeService.subscriptionId not found!")
162+
}
163+
164+
subscription = subscriptions.first().subscription()
165+
return subscription
166+
}
167+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright ish group pty ltd 2024.
3+
*
4+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
5+
*
6+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
7+
*/
8+
9+
package ish.oncourse.server.services.chargebee.property
10+
11+
import ish.common.chargebee.ChargebeePropertyType
12+
13+
import javax.sql.DataSource
14+
import java.text.SimpleDateFormat
15+
16+
abstract class ChargebeePropertyProcessor {
17+
private static final SimpleDateFormat SQL_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
18+
19+
private Date startDate
20+
private Date endDate
21+
22+
ChargebeePropertyProcessor(Date startDate, Date endDate) {
23+
this.startDate = startDate
24+
this.endDate = endDate
25+
}
26+
27+
protected String getFormattedStartDate(){
28+
return SQL_DATE_FORMAT.format(startDate)
29+
}
30+
31+
protected String getFormattedEndDate() {
32+
return SQL_DATE_FORMAT.format(endDate)
33+
}
34+
35+
abstract BigDecimal getValue(DataSource dataSource)
36+
abstract ChargebeePropertyType getType()
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright ish group pty ltd 2024.
3+
*
4+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
5+
*
6+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
7+
*/
8+
9+
package ish.oncourse.server.services.chargebee.property
10+
11+
import ish.oncourse.server.util.DbConnectionUtils
12+
13+
import javax.sql.DataSource
14+
15+
abstract class ChargebeeSimplePropertyProcessor extends ChargebeePropertyProcessor {
16+
17+
ChargebeeSimplePropertyProcessor(Date startDate, Date endDate) {
18+
super(startDate, endDate)
19+
}
20+
21+
@Override
22+
BigDecimal getValue(DataSource dataSource) {
23+
return DbConnectionUtils.getBigDecimalForDbQuery(getQuery(), dataSource)
24+
}
25+
26+
abstract String getQuery()
27+
}

0 commit comments

Comments
 (0)