Skip to content

Commit

Permalink
OD-18978 billing scripts restructure (#1260)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
dmitars and GFilipovich authored Sep 23, 2024
1 parent a63f44f commit 76f59d0
Show file tree
Hide file tree
Showing 26 changed files with 819 additions and 1 deletion.
2 changes: 2 additions & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ dependencies {
api 'com.nimbusds:nimbus-jose-jwt:8.9'
implementation 'com.bugsnag:bugsnag:3.6.2'

implementation 'com.chargebee:chargebee-java:3.19.0'

testImplementation "org.apache.cayenne:cayenne-dbsync:$cayenneVersion"
testImplementation 'org.mockito:mockito-core:2.18.3'
testImplementation 'commons-dbcp:commons-dbcp:1.4'
Expand Down
18 changes: 18 additions & 0 deletions server/src/main/groovy/ish/oncourse/server/cayenne/Settings.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright ish group pty ltd 2024.
*
* 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.
*
* 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.
*/

package ish.oncourse.server.cayenne

import ish.oncourse.server.cayenne.glue._Settings

class Settings extends _Settings implements Queueable {
@Override
boolean isAsyncReplicationAllowed() {
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright ish group pty ltd 2024.
*
* 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.
*
* 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.
*/

package ish.oncourse.server.services.chargebee;

import com.google.inject.Provides;
import com.google.inject.Singleton;
import io.bootique.ConfigModule;
import io.bootique.config.ConfigurationFactory;
import ish.oncourse.server.ICayenneService;
import ish.oncourse.server.PreferenceController;

public class ChargebeeModule extends ConfigModule {

@Singleton
@Provides
public ChargebeeService createChargebeeService(ConfigurationFactory configFactory, ICayenneService cayenneService,
PreferenceController preferenceController) {
return configFactory.config(ChargebeeService.class, getConfigPrefix())
.createChargebeeService(cayenneService, preferenceController);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright ish group pty ltd 2024.
*
* 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.
*
* 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.
*/

package ish.oncourse.server.services.chargebee

class ChargebeeQueryUtils {

public static final String TOTAL_CREDIT_PAYMENT_AMOUNT_QUERY_FORMAT = "SELECT SUM(p.amount) AS value" +
" FROM %s p JOIN PaymentMethod pm on p.paymentMethodId = pm.id" +
" WHERE pm.type = 2 " +
" AND p.createdOn >= '%s'" +
" AND p.createdOn < '%s'" +
" AND p.status IN (3, 6)"

public static final String TOTAL_CREDIT_PAYMENT_COUNT_QUERY_FORMAT = "SELECT COUNT(*) AS value" +
" FROM %s p JOIN PaymentMethod pm on p.paymentMethodId = pm.id" +
" WHERE pm.type = 2 " +
" AND p.createdOn >= '%s'" +
" AND p.createdOn < '%s'" +
" AND p.status IN (3, 6)"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright ish group pty ltd 2024.
*
* 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.
*
* 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.
*/

package ish.oncourse.server.services.chargebee

import com.google.inject.Inject
import io.bootique.annotation.BQConfigProperty
import ish.common.chargebee.ChargebeePropertyType
import ish.oncourse.server.ICayenneService
import ish.oncourse.server.PreferenceController
import ish.oncourse.server.cayenne.Preference
import org.apache.cayenne.query.ObjectSelect

class ChargebeeService {
private Boolean localMode = null


@BQConfigProperty
void setLocalMode(Boolean localMode) {
this.localMode = localMode
}

Boolean getLocalMode() {
return localMode
}


private ICayenneService cayenneService
private PreferenceController preferenceController


String getSubscriptionId(){
return preferenceController.getChargebeeSubscriptionId()
}

List<String> getAllowedAddons() {
def addons = preferenceController.getChargebeeAllowedAddons()
if(addons == null)
return new ArrayList<String>()

return addons.split(ChargebeePropertyType.ADDONS_SEPARATOR)?.toList()
}

String configOf(ChargebeePropertyType type) {
def preference = ObjectSelect.query(Preference)
.where(Preference.NAME.eq(type.getDbPropertyName()))
.selectOne(cayenneService.newContext)

if(preference == null)
throw new IllegalStateException("Attempt to upload $type property to chargebee, but config was not replicated for this college")

return preference.getValueString()
}


ChargebeeService createChargebeeService(ICayenneService cayenneService, PreferenceController preferenceController) {
this.cayenneService = cayenneService
this.preferenceController = preferenceController
this
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Copyright ish group pty ltd 2024.
*
* 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.
*
* 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.
*/

package ish.oncourse.server.services.chargebee

import com.chargebee.Environment
import com.chargebee.models.Subscription
import com.chargebee.models.Usage
import com.google.inject.Inject
import ish.common.chargebee.ChargebeePropertyType
import ish.oncourse.server.ICayenneService
import ish.oncourse.server.PreferenceController
import ish.oncourse.server.cayenne.Script
import ish.oncourse.server.messaging.MessageService
import ish.oncourse.server.scripting.api.EmailService
import ish.oncourse.server.scripting.api.EmailSpec
import ish.oncourse.server.scripting.api.MessageSpec
import ish.oncourse.server.services.AuditService
import ish.oncourse.server.services.chargebee.property.ChargebeePropertyProcessor
import ish.oncourse.server.services.chargebee.property.ChargeebeeProcessorFactory
import ish.oncourse.types.AuditAction
import org.apache.cayenne.query.ObjectSelect
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.quartz.DisallowConcurrentExecution
import org.quartz.Job
import org.quartz.JobExecutionContext
import org.quartz.JobExecutionException

import java.sql.Timestamp
import java.time.Instant

@DisallowConcurrentExecution
class ChargebeeUploadJob implements Job {
private static final Logger logger = LogManager.getLogger()

@Inject
private ICayenneService cayenneService

@Inject
private ChargebeeService chargebeeService

@Inject
private MessageService messageService

@Inject
private PreferenceController preferenceController

@Inject
private AuditService auditService

private static Subscription subscription = null


@Override
void execute(JobExecutionContext context) throws JobExecutionException {
logger.warn("ChargebeeUploadJob started")

def addons = chargebeeService.getAllowedAddons()
if(addons.isEmpty()) {
logger.warn("ChargebeeUploadJob is rejected due to allowed addons not configured for this college")
return
}

def site = chargebeeService.configOf(ChargebeePropertyType.SITE)
def apiKey = chargebeeService.configOf(ChargebeePropertyType.API_KEY)


if (site == null || apiKey == null) {
String error = "Try to use chargebee, but its configs don't have necessary field (site or api key)"
logger.error(error)
throw new RuntimeException(error)
}

Calendar aCalendar = Calendar.getInstance()
aCalendar.add(Calendar.MONTH, -1)
aCalendar.set(Calendar.DATE, 1)
def firstDateOfPreviousMonth = aCalendar.getTime()

aCalendar.add(Calendar.MONTH, 1)
aCalendar.set(Calendar.DATE, 1)
def firstDateOfCurrentMonth = aCalendar.getTime()

def propertiesToUpload = ChargebeePropertyType.getItems()
.findAll {addons.contains(it.getDbPropertyName())}

logger.warn("Chargebee start date including $firstDateOfPreviousMonth , end date $firstDateOfCurrentMonth")

try {
if(!chargebeeService.localMode)
Environment.configure(site, apiKey)

propertiesToUpload.each { type ->
def property = ChargeebeeProcessorFactory.valueOf(type, firstDateOfPreviousMonth, firstDateOfCurrentMonth)
uploadUsageToSite(property)
}
} catch (Exception e) {
logger.catching(e)
throw e
}

logger.warn("ChargeebeeUploadJob executed successfully")
}


private void uploadUsageToSite(ChargebeePropertyProcessor propertyProcessor) {
if(propertyProcessor.type == null) {
throw new IllegalArgumentException("Try to upload chargebee usage without item type")
}

String itemPriceId = chargebeeService.configOf(propertyProcessor.type)
if(itemPriceId == null)
throw new IllegalArgumentException("Try to upload usage $propertyProcessor.type without configured item id")

def quantity = propertyProcessor.getValue(cayenneService.dataSource)
logger.warn("Try to upload to chargebee $propertyProcessor.type with id $itemPriceId value $quantity")

if(Boolean.TRUE == chargebeeService.localMode)
auditService.submit(ObjectSelect.query(Script).selectFirst(cayenneService.newReadonlyContext), AuditAction.SCRIPT_EXECUTED, "Try to upload to chargebee $propertyProcessor.type with id $itemPriceId value " + quantity.toPlainString())
else {
def subscription = getSubscription()
if(!subscription.subscriptionItems().find {it.itemPriceId() == itemPriceId}) {
logger.warn("Item price id $itemPriceId not allowed for subscription $chargebeeService.subscriptionId and will be ignored")
return
}
uploadToChargebee(itemPriceId, quantity.toPlainString())
}
}

private void uploadToChargebee(String itemPriceId, String quantity) {
try {
Usage.create(chargebeeService.subscriptionId)
.itemPriceId(itemPriceId)
.quantity(quantity)
.usageDate(new Timestamp(Instant.now().toEpochMilli()))
.request()
} catch (Exception e) {
logger.error("Chargebee usage upload error: " + e.getMessage())
messageService.sendMessage(new MessageSpec().with {
it.subject = 'onCourse->Chargebee usage upload error. Contact ish support'
it.content ="\n$itemPriceId upload error for college $preferenceController.collegeName. Reason: $e.message"
it.from(preferenceController.emailFromAddress)
it.to("[email protected]")
it
})
}
}

private Subscription getSubscription(){
if(subscription != null)
return subscription


def subscriptions = Subscription.list().id().is(chargebeeService.subscriptionId).request()
if(subscriptions.empty) {
throw new IllegalArgumentException("Subscription with id $chargebeeService.subscriptionId not found!")
}

subscription = subscriptions.first().subscription()
return subscription
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright ish group pty ltd 2024.
*
* 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.
*
* 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.
*/

package ish.oncourse.server.services.chargebee.property

import ish.common.chargebee.ChargebeePropertyType

import javax.sql.DataSource
import java.text.SimpleDateFormat

abstract class ChargebeePropertyProcessor {
private static final SimpleDateFormat SQL_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

private Date startDate
private Date endDate

ChargebeePropertyProcessor(Date startDate, Date endDate) {
this.startDate = startDate
this.endDate = endDate
}

protected String getFormattedStartDate(){
return SQL_DATE_FORMAT.format(startDate)
}

protected String getFormattedEndDate() {
return SQL_DATE_FORMAT.format(endDate)
}

abstract BigDecimal getValue(DataSource dataSource)
abstract ChargebeePropertyType getType()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright ish group pty ltd 2024.
*
* 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.
*
* 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.
*/

package ish.oncourse.server.services.chargebee.property

import ish.oncourse.server.util.DbConnectionUtils

import javax.sql.DataSource

abstract class ChargebeeSimplePropertyProcessor extends ChargebeePropertyProcessor {

ChargebeeSimplePropertyProcessor(Date startDate, Date endDate) {
super(startDate, endDate)
}

@Override
BigDecimal getValue(DataSource dataSource) {
return DbConnectionUtils.getBigDecimalForDbQuery(getQuery(), dataSource)
}

abstract String getQuery()
}
Loading

0 comments on commit 76f59d0

Please sign in to comment.