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