diff --git a/client/conf/freeswitch-conf-endaga/freeswitch/chatplan/default/00_vbts-chat.xml b/client/conf/freeswitch-conf-endaga/freeswitch/chatplan/default/00_vbts-chat.xml
index 8f8dcec8..39648761 100644
--- a/client/conf/freeswitch-conf-endaga/freeswitch/chatplan/default/00_vbts-chat.xml
+++ b/client/conf/freeswitch-conf-endaga/freeswitch/chatplan/default/00_vbts-chat.xml
@@ -33,6 +33,11 @@ of patent rights can be found in the PATENTS file in the same directory.
+
+
+
+
+
diff --git a/client/conf/freeswitch-conf-endaga/freeswitch/chatplan/default/23_number_status.xml b/client/conf/freeswitch-conf-endaga/freeswitch/chatplan/default/23_number_status.xml
new file mode 100644
index 00000000..fb1180c9
--- /dev/null
+++ b/client/conf/freeswitch-conf-endaga/freeswitch/chatplan/default/23_number_status.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/conf/freeswitch-conf-endaga/freeswitch/dialplan/default/00_vbts-call-internal.xml b/client/conf/freeswitch-conf-endaga/freeswitch/dialplan/default/00_vbts-call-internal.xml
index 74b5901c..ce7e3216 100644
--- a/client/conf/freeswitch-conf-endaga/freeswitch/dialplan/default/00_vbts-call-internal.xml
+++ b/client/conf/freeswitch-conf-endaga/freeswitch/dialplan/default/00_vbts-call-internal.xml
@@ -24,6 +24,9 @@ of patent rights can be found in the PATENTS file in the same directory.
+
+
+
diff --git a/client/conf/freeswitch-conf-endaga/freeswitch/dialplan/default/26_number_status.xml b/client/conf/freeswitch-conf-endaga/freeswitch/dialplan/default/26_number_status.xml
new file mode 100644
index 00000000..785068a0
--- /dev/null
+++ b/client/conf/freeswitch-conf-endaga/freeswitch/dialplan/default/26_number_status.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/core/checkin.py b/client/core/checkin.py
index dddfedf0..b3e2e23c 100644
--- a/client/core/checkin.py
+++ b/client/core/checkin.py
@@ -92,6 +92,7 @@ def process_config(self, config_dict):
@delta.DeltaCapable(section_ctx['subscribers'], True)
def process_subscribers(self, data_dict):
subscriber.process_update(data_dict)
+ subscriber.status(update=data_dict)
def process_events(self, data_dict):
"""Process information about events.
diff --git a/client/core/config_database.py b/client/core/config_database.py
index ae82dbe6..0e7d0981 100644
--- a/client/core/config_database.py
+++ b/client/core/config_database.py
@@ -154,8 +154,9 @@ def set_defaults(force_replace=False):
'external_interface': 'tun0',
# The internal interface is the NIC used by the BSC/BTS to address this
# system
- 'internal_interface': 'lo'
-
+ 'internal_interface': 'lo',
+ # Network Max Permissible Transaction
+ 'network_mput': 3
}
config = ConfigDB()
diff --git a/client/core/events.py b/client/core/events.py
index 3892f43b..4a68b464 100644
--- a/client/core/events.py
+++ b/client/core/events.py
@@ -28,7 +28,7 @@ def usage(num=100):
def kind_from_reason(reason_str):
types = ["local_call", "local_sms", "outside_call", "outside_sms",
"free_call", "free_sms", "incoming_sms", "error_sms",
- "error_call", "transfer", "add_money", "deduct_money",
+ "error_call", "error_transfer", "transfer", "add_money", "deduct_money",
"set_balance", "unknown", "Provisioned", "local_recv_call",
"local_recv_sms", "incoming_call", "gprs"]
for t in types:
diff --git a/client/scripts/freeswitch/VBTS_Get_Account_Status.py b/client/scripts/freeswitch/VBTS_Get_Account_Status.py
new file mode 100644
index 00000000..db7787e1
--- /dev/null
+++ b/client/scripts/freeswitch/VBTS_Get_Account_Status.py
@@ -0,0 +1,96 @@
+"""Get a subscriber's account balance via their IMSI.
+
+Copyright (c) 2016-present, Facebook, Inc.
+All rights reserved.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree. An additional grant
+of patent rights can be found in the PATENTS file in the same directory.
+"""
+
+from freeswitch import consoleLog
+
+from core.subscriber import subscriber
+from core.subscriber.base import SubscriberNotFound
+
+
+def chat(message, args):
+ """Handle chat requests.
+
+ Args:
+ string of the form |
+
+ Subscriber State can be:
+ active (unblocked), -active (blocked),first_expired (validity expired)
+ """
+ args = args.split('|')
+ imsi = args[0]
+ dest_imsi = False
+
+ if len(args) > 1:
+ dest_imsi = True
+ if len(imsi) < 4: # Toll Free Numbers don't have imsis
+ subscriber_state = 'active'
+ else:
+ subscriber_state = str(
+ subscriber.status().get_account_status(imsi)).lower()
+ else:
+ subscriber_state = str(
+ subscriber.status().get_account_status(imsi)).lower()
+ try:
+ account_status = False
+ if not dest_imsi:
+ if 'active' == subscriber_state:
+ account_status = True
+ else:
+ # incoming number status
+ allowed_states = ['active', 'active*',
+ 'first_expired', 'first_expired*']
+ if subscriber_state in allowed_states:
+ account_status = True
+
+ except SubscriberNotFound:
+ account_status = False
+ consoleLog('info', "Returned Chat:" + str(account_status) + "\n")
+ message.chat_execute('set', '_openbts_ret=%s' % account_status)
+
+
+def fsapi(session, stream, env, args):
+ """Handle FS API requests.
+
+ Args:
+ string of the form |
+
+ Subscriber State can be:
+ active (unblocked), -active (blocked),first_expired (validity expired)
+ """
+ args = args.split('|')
+ imsi = args[0]
+ dest_imsi = False
+
+ if len(args) > 1:
+ dest_imsi = True
+ if len(imsi) < 4: # Toll Free Numbers don't have imsis
+ subscriber_state = 'active'
+ else:
+ subscriber_state = str(
+ subscriber.status().get_account_status(imsi)).lower()
+ else:
+ subscriber_state = str(
+ subscriber.status().get_account_status(imsi)).lower()
+ try:
+ account_status = False
+ if not dest_imsi:
+ if 'active' == subscriber_state:
+ account_status = True
+ else:
+ # incoming number status
+ allowed_states = ['active', 'active*',
+ 'first_expired', 'first_expired*']
+ if subscriber_state in allowed_states:
+ account_status = True
+
+ except SubscriberNotFound:
+ account_status = False
+ consoleLog('info', "Returned FSAPI: " + str(account_status) + "\n")
+ stream.write(str(account_status))
\ No newline at end of file
diff --git a/cloud/endagaweb/celery.py b/cloud/endagaweb/celery.py
index 8125f76f..3adb1957 100644
--- a/cloud/endagaweb/celery.py
+++ b/cloud/endagaweb/celery.py
@@ -42,5 +42,17 @@
'task': 'endagaweb.tasks.usageevents_to_sftp',
# Run this at 15:00 UTC (10:00 PDT, 02:00 Papua time)
'schedule': crontab(minute=0, hour=17),
+ },'unblock-blocked-subscriber': {
+ 'task': 'endagaweb.tasks.unblock_blocked_subscribers',
+ # Run this in every minute
+ 'schedule': crontab(minute='*'),
+ },'validity-expiry-sms': {
+ 'task': 'endagaweb.tasks.validity_expiry_sms',
+ # Run this at 14:00 UTC (09:00 PDT, 01:00 Papua time).
+ 'schedule': crontab(minute=0, hour=15),
+ },'subscriber-validity-state': {
+ 'task': 'endagaweb.tasks.subscriber_validity_state',
+ # Run this prior to vacuuming of subscribers.
+ 'schedule': crontab(minute=58, hour=16),
}
})
diff --git a/cloud/endagaweb/checkin.py b/cloud/endagaweb/checkin.py
index efa19311..ec96eee7 100644
--- a/cloud/endagaweb/checkin.py
+++ b/cloud/endagaweb/checkin.py
@@ -26,7 +26,7 @@
from endagaweb.models import TimeseriesStat
from endagaweb.models import UsageEvent
from endagaweb.util.parse_destination import parse_destination
-
+import dateutil.parser as dateparser
class CheckinResponder(object):
@@ -56,6 +56,7 @@ def __init__(self, bts):
'system_utilization': self.timeseries_handler,
'subscribers': self.subscribers_handler,
'radio': self.radio_handler, # needs location_handler -kurtis
+ 'subscriber_status': self.subscriber_status_handler,
# TODO: (kheimerl) T13270418 Add location update information
}
@@ -264,6 +265,31 @@ def subscribers_handler(self, subscribers):
(imsi, ))
continue
+ def subscriber_status_handler(self, subscriber_status):
+ """
+ Update the subscribers' state and validity info based on
+ what the client submits.
+ """
+ for imsi in subscriber_status:
+ sub_info = json.loads(subscriber_status[imsi]['state'])
+ validity_now = str(sub_info['validity'])
+ state = str(sub_info['state'])
+ try:
+ sub = Subscriber.objects.get(imsi=imsi)
+ if sub.valid_through.date() < dateparser.parse(validity_now).date():
+ sub.state = 'active'
+ sub.valid_through = validity_now
+ if state == 'active*':
+ sub.is_blocked = True
+ evt_gen = UsageEvent.objects.filter(
+ kind='error_transfer').order_by('-date')[0]
+ sub.last_blocked = evt_gen.date
+ sub.save()
+ except Subscriber.DoesNotExist:
+ logging.warn('[subscriber_status_handler] subscriber %s does not'
+ ' exist.' % imsi)
+
+
def radio_handler(self, radio):
if 'band' in radio and 'c0' in radio:
self.bts.update_band_and_channel(radio['band'], radio['c0'])
@@ -271,12 +297,17 @@ def radio_handler(self, radio):
def gen_subscribers(self):
"""
Returns a list of active subscribers for a network, along with
- PN-counter for each sub containing last known balance.
+ PN-counter for each sub containing last known balance and state.
"""
res = {}
for s in Subscriber.objects.filter(network=self.bts.network):
bal = crdt.PNCounter.from_state(json.loads(s.crdt_balance))
- data = {'numbers': s.numbers_as_list(), 'balance': bal.state}
+ state = str(s.state)
+ if s.is_blocked:
+ # append '*' if subscriber is blocked, even if in active state
+ state = state + '*'
+ data = {'numbers': s.numbers_as_list(), 'balance': bal.state,
+ 'state': state, 'validity': str(s.valid_through.date())}
res[s.imsi] = data
return res
@@ -389,6 +420,7 @@ def gen_config(self):
# pylint: disable=no-member
result['endaga']['number_country'] = self.bts.network.number_country
result['endaga']['currency_code'] = self.bts.network.subscriber_currency
+ result['endaga']['network_mput'] = self.bts.network.max_failure_transaction
# Get the latest versions available on each channel.
latest_stable_version = ClientRelease.objects.filter(
channel='stable').order_by('-date')[0].version
diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py
index 65b0780f..39c80cdf 100644
--- a/cloud/endagaweb/models.py
+++ b/cloud/endagaweb/models.py
@@ -24,12 +24,13 @@
from django.contrib.auth.models import Group, User
from django.contrib.gis.db import models as geomodels
from django.core.validators import MinValueValidator
+from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.db import connection
from django.db import models
from django.db import transaction
from django.db.models import F
-from django.db.models.signals import post_save
+from django.db.models.signals import post_save, pre_save
from guardian.shortcuts import (assign_perm, get_users_with_perms)
from rest_framework.authtoken.models import Token
import django.utils.timezone
@@ -58,7 +59,11 @@
OUTBOUND_ACTIVITIES = (
'outside_call', 'outside_sms', 'local_call', 'local_sms',
)
-
+# These UsageEvent events are not allowed block the Subscriber if repeated
+# more than Maximum Permissible Unsuccessful Transactions
+INVALID_EVENTS = (
+ 'error_transfer',
+)
class UserProfile(models.Model):
"""UserProfiles extend the default Django User models.
@@ -514,7 +519,7 @@ class Subscriber(models.Model):
imsi = models.CharField(max_length=50, unique=True)
name = models.TextField()
crdt_balance = models.TextField(default=crdt.PNCounter("default").serialize())
- state = models.CharField(max_length=10)
+ state = models.CharField(max_length=15, default='first_expired')
# Time of the last received UsageEvent that's not in NON_ACTIVITIES.
last_active = models.DateTimeField(null=True, blank=True)
# Time of the last received UsageEvent that is in OUTBOUND_ACTIVITIES. We
@@ -525,6 +530,19 @@ class Subscriber(models.Model):
# When toggled, this will protect a subsriber from getting "vacuumed." You
# can still delete subs with the usual "deactivate" button.
prevent_automatic_deactivation = models.BooleanField(default=False)
+ # Block subscriber if repeated unauthorized events.
+ is_blocked = models.BooleanField(default=False)
+ # older validity until first recharge
+ valid_through = models.DateTimeField(null=True,
+ default=django.utils.timezone.now()
+ - datetime.timedelta(days=1))
+ block_reason = models.TextField(default='N/A', max_length=255)
+ last_blocked = models.DateTimeField(null=True, blank=True)
+ # role of subscriber
+ role = models.TextField(null=True, blank=True, default="subscriber")
+
+ class Meta:
+ default_permissions = ()
@classmethod
def update_balance(cls, imsi, other_bal):
@@ -576,8 +594,8 @@ def change_balance(self, amt):
self.crdt_balance = bal.serialize()
def __unicode__(self):
- return "Sub %s, %s, network: %s, balance: %d" % (
- self.name, self.imsi, self.network, self.balance)
+ return "Sub %s, %s, network: %s, balance: %d, role: %s" % (
+ self.name, self.imsi, self.network, self.balance, self.role)
def numbers(self):
n = self.number_set.all()
@@ -765,7 +783,7 @@ class UsageEvent(models.Model):
downloaded_bytes: number of downloaded bytes for a GPRS event
timespan: the duration of time over which the GPRS data was sampled
"""
- transaction_id = models.UUIDField(editable=False, default=uuid.uuid4)
+ transaction_id = models.TextField()
subscriber = models.ForeignKey(Subscriber, null=True,
on_delete=models.SET_NULL)
subscriber_imsi = models.TextField(null=True)
@@ -868,10 +886,76 @@ def set_subscriber_last_active(sender, instance=None, created=False,
event.subscriber.last_active = event.date
event.subscriber.save()
+ @staticmethod
+ def if_invalid_events(sender, instance=None, created=False, **kwargs):
+ # Check for any invalid event and make an entry
+ if not created:
+ return
+ event = instance
+ if event.kind in INVALID_EVENTS:
+ if SubscriberInvalidEvents.objects.filter(
+ subscriber=event.subscriber).exists():
+ # Subscriber is blocked after N(max_failure_transaction)
+ # counts
+ sub_evt = SubscriberInvalidEvents.objects.get(
+ subscriber=event.subscriber)
+ # if it hits max_failure_trx of Network in 24hr
+ # block the subscriber
+ negative_transactions_ids = sub_evt .negative_transactions + [
+ event.transaction_id]
+ sub_evt.count = sub_evt.count + 1
+ sub_evt.event_time = event.date
+ sub_evt.negative_transactions = negative_transactions_ids
+ sub_evt.save()
+ max_transactions = event.subscriber.network.max_failure_transaction
+ if sub_evt.count >= max_transactions:
+ block_reason = 'Repeated %s within 24 hours ' % (
+ '/'.join(INVALID_EVENTS),)
+ # event.subscriber.is_blocked = True (already blocked on
+ event.subscriber.block_reason = block_reason
+ if sub_evt.count == max_transactions:
+ # Update time for last max failure trx event only
+ event.subscriber.last_blocked = django.utils.timezone.now()
+ event.subscriber.save()
+ logger.info('Subscriber %s blocked for 30 minutes, '
+ 'repeated invalid transactions within 24 '
+ 'hours' % event.subscriber_imsi)
+ else:
+ sub_evt = SubscriberInvalidEvents.objects.create(
+ subscriber=event.subscriber, count=1)
+ sub_evt.event_time = event.date
+ sub_evt.negative_transactions = [event.transaction_id]
+ sub_evt.save()
+ elif SubscriberInvalidEvents.objects.filter(
+ subscriber=event.subscriber).count() > 0:
+ # Delete the event if events are non-consecutive keep the event if
+ # until subscriber is unblocked
+ if not event.subscriber.is_blocked:
+ sub_evt = SubscriberInvalidEvents.objects.get(
+ subscriber=event.subscriber)
+ logger.info('Subscriber %s invalid event removed' % (
+ event.subscriber_imsi))
+ sub_evt.delete()
+
+ @staticmethod
+ def set_transaction_id(sender, instance=None, **kwargs):
+ """
+ Create transaction id to some readable format
+ Set transaction as negative transaction if error event
+ """
+ event = instance
+ if event.kind in INVALID_EVENTS:
+ negative = True
+ else:
+ negative = False
+ event.transaction_id = dbutils.format_transaction(instance.date,
+ negative)
+
post_save.connect(UsageEvent.set_imsi_and_uuid_and_network, sender=UsageEvent)
post_save.connect(UsageEvent.set_subscriber_last_active, sender=UsageEvent)
-
+post_save.connect(UsageEvent.if_invalid_events, sender=UsageEvent)
+pre_save.connect(UsageEvent.set_transaction_id, sender=UsageEvent)
class PendingCreditUpdate(models.Model):
"""A credit update that has yet to be acked by a BTS.
@@ -1793,3 +1877,11 @@ class FileUpload(models.Model):
created_time = models.DateTimeField(auto_now_add=True)
modified_time = models.DateTimeField(auto_now_add=True)
accessed_time = models.DateTimeField(auto_now=True)
+
+
+class SubscriberInvalidEvents(models.Model):
+ """ Invalid Events logs by Subscriber"""
+ subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE)
+ count = models.PositiveIntegerField()
+ event_time = models.DateTimeField(auto_now_add=True)
+ negative_transactions = ArrayField(models.TextField(), null=True)
diff --git a/cloud/endagaweb/tasks.py b/cloud/endagaweb/tasks.py
index 405ca698..0342f4db 100644
--- a/cloud/endagaweb/tasks.py
+++ b/cloud/endagaweb/tasks.py
@@ -9,6 +9,7 @@
"""
from __future__ import absolute_import
+from endagaweb.celery import app as celery_app
import csv
import datetime
@@ -38,7 +39,7 @@
from endagaweb.models import Network
from endagaweb.models import PendingCreditUpdate
from endagaweb.models import ConfigurationKey
-from endagaweb.models import Subscriber
+from endagaweb.models import Subscriber, SubscriberInvalidEvents
from endagaweb.models import UsageEvent
from endagaweb.models import SystemEvent
from endagaweb.models import TimeseriesStat
@@ -246,11 +247,11 @@ def vacuum_inactive_subscribers(self):
# Do nothing if subscriber vacuuming is disabled for the network.
if not network.sub_vacuum_enabled:
continue
- inactives = network.get_outbound_inactive_subscribers(
- network.sub_vacuum_inactive_days)
+ inactives = Subscriber.objects.filter(
+ state='recycle', network_id=network.id,
+ prevent_automatic_deactivation=False
+ )
for subscriber in inactives:
- if subscriber.prevent_automatic_deactivation:
- continue
print 'vacuuming %s from network %s' % (subscriber.imsi, network)
subscriber.deactivate()
# Sleep a bit in between each deactivation so we don't flood the
@@ -439,3 +440,170 @@ def req_bts_log(self, obj, retry_delay=60*10, max_retries=432):
raise
finally:
obj.save()
+
+
+@app.task(bind=True)
+def unblock_blocked_subscribers(self):
+ """Unblock subscribers who are blocked for past 30 minutes.
+ This runs this as a periodic task managed by celerybeat.
+ """
+ unblock_time = django.utils.timezone.now() - datetime.timedelta(minutes=30)
+ clear_evt_time = django.utils.timezone.now() - datetime.timedelta(days=1)
+ subscribers = Subscriber.objects.filter(is_blocked=True,
+ last_blocked__lte=unblock_time)
+ if not subscribers:
+ return # Do nothing
+ imsis = [subscriber.imsi for subscriber in subscribers]
+ print '%s was blocked for past 30 minutes now Unblocked!' % (
+ imsis, )
+ subscribers.update(is_blocked=False, block_reason='N/A')
+ # Clear Invalid Events History
+ sub_evt = SubscriberInvalidEvents.objects.filter(
+ event_time__lte=clear_evt_time)
+ if sub_evt:
+ sub_evt.delete()
+ body = 'You number is unblocked and services are resumed!'
+ for sub in subscribers:
+ try:
+ # We send sms to the subscriber's first number.
+ num = sub.number_set.all()[0] if not sub.is_blocked else None
+ except IndexError:
+ num = None
+ if num:
+ # Send unblock sms to the number
+ celery_app.send_task('endagaweb.tasks.sms_notification',
+ (body, num))
+
+
+@app.task(bind=True)
+def subscriber_validity_state(self):
+ """
+ Updates the subscribers state to first_expired/expired/recycle
+ state is set to 'Active' only when top-up.
+ Ignored for Retailers
+ """
+
+ today = django.utils.timezone.now().date()
+ subscribers = Subscriber.objects.filter(
+ valid_through__lte=today).exclude(role='retailer')
+ if subscribers:
+ for subscriber in subscribers:
+ try:
+ if subscriber.valid_through is None:
+ continue
+ except IndexError:
+ continue
+
+ subscriber_validity = subscriber.valid_through.date()
+ subscriber_state = str(subscriber.state)
+ first_expire = subscriber_validity + datetime.timedelta(
+ days=subscriber.network.sub_vacuum_inactive_days)
+ expired = first_expire + datetime.timedelta(
+ days=subscriber.network.sub_vacuum_grace_days)
+
+ if subscriber_validity < today:
+ if today <= first_expire:
+ if subscriber_state.lower() != 'first_expired':
+ subscriber.state = 'first_expired'
+ subscriber.save()
+ elif today <= expired:
+ if subscriber_state.lower() != 'expired':
+ subscriber.state = 'expired'
+ subscriber.save()
+ else:
+ # Let deactivation of subscriber be handled by
+ # vacuum_inactive_subscribers
+ if subscriber_state.lower() != 'recycle':
+ subscriber.state = 'recycle'
+ subscriber.save()
+
+ print "Updating Subscriber: %s state to %s " \
+ % (subscriber.imsi, subscriber.state)
+ # Create a Usage event
+ if subscriber.state in ['first_expired', 'expired', 'recycle']:
+ now = django.utils.timezone.now()
+ info = 'Validity expired setting state as %s' \
+ % subscriber.state
+ event = UsageEvent.objects.create(
+ subscriber=subscriber, date=now, bts=subscriber.bts,
+ reason=info, oldamt=subscriber.balance,
+ newamt=subscriber.balance, change=0)
+ event.save()
+
+
+@app.task(bind=True)
+def validity_expiry_sms(self, days=7):
+ """Sends SMS to the number whose validity is:
+ about to get expire,
+ if expired (i.e 1st expired), or
+ if the number is in grace period and is about to recycle.
+
+ Args:
+ days: Days prior (state change) which the SMS is sent to Subscriber.
+ Runs as everyday task managed by celerybeat.
+ """
+ today = django.utils.timezone.datetime.now().date()
+ for subscriber in Subscriber.objects.iterator():
+ # Do nothing if subscriber vacuuming is disabled for the network.
+ if not subscriber.network.sub_vacuum_enabled:
+ continue
+ try:
+ number = subscriber.number_set.all()[0]
+ subscriber_validity = subscriber.valid_through
+ # In case where number has no validity
+ if subscriber_validity is None:
+ print '%s has no validity' % (subscriber.imsi,)
+ continue
+ except IndexError:
+ print 'No number attached to subscriber %s' % (subscriber.imsi,)
+ continue
+
+ subscriber_validity = subscriber_validity.date()
+ inactive_period = subscriber.network.sub_vacuum_inactive_days
+ grace_period = subscriber.network.sub_vacuum_grace_days
+
+ prior_first_expire = subscriber_validity + datetime.timedelta(
+ days=inactive_period) - datetime.timedelta(days=days)
+
+ prior_recycle = prior_first_expire + datetime.timedelta(
+ days=grace_period)
+
+ # Prior to expiry state (one on last day and before defined days)
+ if subscriber_validity > today and (
+ (subscriber_validity - datetime.timedelta(
+ days=days)
+ ) == today or today == (
+ subscriber_validity - datetime.timedelta(
+ days=1)) or today == subscriber_validity):
+ body = 'Your validity is about to get expired on %s , Please ' \
+ 'recharge to continue the service. Please ignore if ' \
+ 'already done! ' % (subscriber_validity,)
+ celery_app.send_task('endagaweb.tasks.sms_notification',
+ (body, number))
+ # Prior 1st_expired state
+ elif subscriber_validity < today:
+ if prior_first_expire == today or today == (
+ prior_first_expire + datetime.timedelta(
+ days=days - 1)):
+ body = 'Your validity has expired on %s, Please recharge ' \
+ 'immediately to activate your services again! ' % (
+ subscriber_validity,)
+ celery_app.send_task('endagaweb.tasks.sms_notification',
+ (body, number))
+ # Prior to recycle state
+ elif prior_recycle == today or today == (
+ prior_recycle + datetime.timedelta(days=days - 1)):
+ body = 'Warning: Your validity has expired on %s , Please ' \
+ 'recharge immediately to avoid deactivation of your ' \
+ 'connection! ' % (subscriber_validity,)
+ celery_app.send_task('endagaweb.tasks.sms_notification',
+ (body, number))
+ # SMS on same day of expiry
+ elif subscriber_validity == today:
+ body = 'Your validity expiring today %s, Please recharge ' \
+ 'immediately to continue your services again!, ' \
+ 'Ignore if already done! ' % (subscriber_validity,)
+ celery_app.send_task('endagaweb.tasks.sms_notification',
+ (body, number))
+ else:
+ return # Do nothing
diff --git a/cloud/endagaweb/util/dbutils.py b/cloud/endagaweb/util/dbutils.py
index 42e0e7ee..56f4318b 100644
--- a/cloud/endagaweb/util/dbutils.py
+++ b/cloud/endagaweb/util/dbutils.py
@@ -7,9 +7,27 @@
LICENSE file in the root directory of this source tree. An additional grant
of patent rights can be found in the PATENTS file in the same directory.
"""
+import uuid
+
+import django
def get_db_time(connection):
cursor = connection.cursor()
cursor.execute("SELECT statement_timestamp();")
return cursor.fetchone()[0]
+
+
+def format_transaction(tansaction_date=None, transaction_type=False):
+ # Generating new transaction id using old transaction and date
+ if tansaction_date is None:
+ dt = django.utils.timezone.now()
+ else:
+ dt = tansaction_date
+ uuid_transaction = str(uuid.uuid4().hex[:6])
+ transaction = '{0}id{1}'.format(str(dt.date()).replace('-', ''),
+ uuid_transaction)
+ if transaction_type:
+ return '-%s' % (transaction,)
+ else:
+ return transaction