From 42c729c1deb77bec74825ec8f43f3e0d75f8f882 Mon Sep 17 00:00:00 2001
From: Robert Anderson <randerson@lbsd.net>
Date: Mon, 24 Aug 2009 12:14:12 +0000
Subject: [PATCH] Fixed accounting start, update and stop records handling
 sessions spanning over multiple periods Added UPGRADING file for upgrading
 information from previous revision

---
 UPGRADING                                     | 124 ++++++
 smradius/constants.pm                         |  14 +-
 .../modules/accounting/mod_accounting_sql.pm  | 411 ++++++++++++++----
 .../modules/features/mod_feature_capping.pm   |   5 +-
 .../modules/system/mod_config_sql_topups.pm   |  25 +-
 smradiusd.conf                                |  70 ++-
 6 files changed, 544 insertions(+), 105 deletions(-)
 create mode 100644 UPGRADING

diff --git a/UPGRADING b/UPGRADING
new file mode 100644
index 00000000..55e6707e
--- /dev/null
+++ b/UPGRADING
@@ -0,0 +1,124 @@
+r509:	
+	# Database
+
+	ALTER TABLE accounting ADD PeriodKey VARCHAR(7);
+	ALTER TABLE accounting_summary DROP COLUMN AcctInputPackets;
+
+	# smradiusd.conf
+
+	Modified:
+	accounting_start_query=<<EOT
+		INSERT INTO
+			@TP@accounting
+		(
+			Username,
+			ServiceType,
+			FramedProtocol,
+			NASPort,
+			NASPortType,
+			CallingStationID,
+			CalledStationID,
+			NASPortID,
+			AcctSessionID,
+			FramedIPAddress,
+			AcctAuthentic,
+			EventTimestamp,
+			AcctStatusType,
+			NASIdentifier,
+			NASIPAddress,
+			AcctDelayTime,
+			PeriodKey
+		)
+		VALUES
+		(
+			%{request.User-Name},
+			%{request.Service-Type},
+			%{request.Framed-Protocol},
+			%{request.NAS-Port},
+			%{request.NAS-Port-Type},
+			%{request.Calling-Station-Id},
+			%{request.Called-Station-Id},
+			%{request.NAS-Port-Id},
+			%{request.Acct-Session-Id},
+			%{request.Framed-IP-Address},
+			%{request.Acct-Authentic},
+			%{request.Timestamp},
+			%{request.Acct-Status-Type},
+			%{request.NAS-Identifier},
+			%{request.NAS-IP-Address},
+			%{request.Acct-Delay-Time},
+			%{query.PeriodKey}
+		)
+	EOT
+
+	accounting_update_query=<<EOT
+		UPDATE
+			@TP@accounting
+		SET
+			AcctSessionTime = %{query.AcctSessionTime},
+			AcctInputOctets = %{query.AcctInputOctets},
+			AcctInputGigawords = %{query.AcctInputGigawords},
+			AcctInputPackets = %{query.AcctInputPackets},
+			AcctOutputOctets = %{query.AcctOutputOctets},
+			AcctOutputGigawords = %{query.AcctOutputGigawords},
+			AcctOutputPackets = %{query.AcctOutputPackets},
+			AcctStatusType = %{request.Acct-Status-Type}
+		WHERE
+			Username = %{request.User-Name}
+			AND AcctSessionID = %{request.Acct-Session-Id}
+			AND NASIPAddress = %{request.NAS-IP-Address}
+			AND PeriodKey = %{query.PeriodKey}
+	EOT
+
+	accounting_stop_query=<<EOT
+		UPDATE
+			@TP@accounting
+		SET
+			AcctSessionTime = %{query.Acct-Session-Time},
+			AcctInputOctets = %{query.Acct-Input-Octets},
+			AcctInputGigawords = %{query.Acct-Input-Gigawords},
+			AcctInputPackets = %{query.Acct-Input-Packets},
+			AcctOutputOctets = %{query.Acct-Output-Octets},
+			AcctOutputGigawords = %{query.Acct-Output-Gigawords},
+			AcctOutputPackets = %{query.Acct-Output-Packets}
+		WHERE
+			Username = %{request.User-Name}
+			AND AcctSessionID = %{request.Acct-Session-Id}
+			AND NASIPAddress = %{request.NAS-IP-Address}
+			AND PeriodKey = %{query.PeriodKey}
+	EOT
+
+	Added:
+	accounting_update_get_records_query=<<EOT
+		SELECT
+			SUM(AcctInputOctets) AS InputOctets,
+			SUM(AcctInputPackets) AS InputPackets,
+			SUM(AcctOutputOctets) AS OutputOctets,
+			SUM(AcctOutputPackets) AS OutputPackets,
+			SUM(AcctInputGigawords) AS InputGigawords,
+			SUM(AcctOutputGigawords) AS OutputGigawords,
+			SUM(AcctSessionTime) AS SessionTime,
+			PeriodKey
+		FROM
+			@TP@accounting
+		WHERE
+			Username = %{request.User-Name}
+			AND AcctSessionID = %{request.Acct-Session-Id}
+			AND NASIPAddress = %{request.NAS-IP-Address}
+		GROUP BY
+			PeriodKey
+		ORDER BY
+			ID ASC
+	EOT
+
+	accounting_stop_status_query=<<EOT
+		UPDATE
+			@TP@accounting
+		SET
+			AcctStatusType = %{request.Acct-Status-Type},
+			AcctTerminateCause = %{request.Acct-Terminate-Cause}
+		WHERE
+			Username = %{request.User-Name}
+			AND AcctSessionID = %{request.Acct-Session-Id}
+			AND NASIPAddress = %{request.NAS-IP-Address}
+	EOT
diff --git a/smradius/constants.pm b/smradius/constants.pm
index 9a4a6e40..1e454161 100644
--- a/smradius/constants.pm
+++ b/smradius/constants.pm
@@ -34,17 +34,21 @@ our (@ISA,@EXPORT,@EXPORT_OK);
 	MOD_RES_ACK
 	MOD_RES_NACK
 	MOD_RES_SKIP
+
+	UINT_MAX
 );
 @EXPORT_OK = ();
 
 
 use constant {
-	RES_OK			=> 0,
-	RES_ERROR		=> -1,
+	RES_OK => 0,
+	RES_ERROR => -1,
 
-	MOD_RES_SKIP		=> 0,
-	MOD_RES_ACK		=> 1,
-	MOD_RES_NACK		=> 2,
+	MOD_RES_SKIP => 0,
+	MOD_RES_ACK => 1,
+	MOD_RES_NACK => 2,
+	
+	UINT_MAX => 2**32
 };
 
 
diff --git a/smradius/modules/accounting/mod_accounting_sql.pm b/smradius/modules/accounting/mod_accounting_sql.pm
index 6469cf14..1e19f891 100644
--- a/smradius/modules/accounting/mod_accounting_sql.pm
+++ b/smradius/modules/accounting/mod_accounting_sql.pm
@@ -28,6 +28,7 @@ use smradius::util;
 
 use POSIX qw(ceil);
 use DateTime;
+use Math::BigInt;
 
 
 # Exporter stuff
@@ -94,7 +95,8 @@ sub init
 			AcctStatusType,
 			NASIdentifier,
 			NASIPAddress,
-			AcctDelayTime
+			AcctDelayTime,
+			PeriodKey
 		)
 		VALUES
 		(
@@ -113,39 +115,74 @@ sub init
 			%{request.Acct-Status-Type},
 			%{request.NAS-Identifier},
 			%{request.NAS-IP-Address},
-			%{request.Acct-Delay-Time}
+			%{request.Acct-Delay-Time},
+			%{query.PeriodKey}
 		)
 	';
 
+	$config->{'accounting_update_get_records_query'} = '
+		SELECT
+			SUM(AcctInputOctets) AS InputOctets,
+			SUM(AcctInputPackets) AS InputPackets,
+			SUM(AcctOutputOctets) AS OutputOctets,
+			SUM(AcctOutputPackets) AS OutputPackets,
+			SUM(AcctInputGigawords) AS InputGigawords,
+			SUM(AcctOutputGigawords) AS OutputGigawords,
+			SUM(AcctSessionTime) AS SessionTime,
+			PeriodKey
+		FROM
+			@TP@accounting
+		WHERE
+			Username = %{request.User-Name}
+			AND AcctSessionID = %{request.Acct-Session-Id}
+			AND NASIPAddress = %{request.NAS-IP-Address}
+		GROUP BY
+			PeriodKey
+		ORDER BY
+			ID ASC
+	';
+
 	$config->{'accounting_update_query'} = '
 		UPDATE
 			@TP@accounting
 		SET
-			AcctSessionTime = %{request.Acct-Session-Time},
-			AcctInputOctets = %{request.Acct-Input-Octets},
-			AcctInputGigawords = %{request.Acct-Input-Gigawords},
-			AcctInputPackets = %{request.Acct-Input-Packets},
-			AcctOutputOctets = %{request.Acct-Output-Octets},
-			AcctOutputGigawords = %{request.Acct-Output-Gigawords},
-			AcctOutputPackets = %{request.Acct-Output-Packets},
+			AcctSessionTime = %{query.SessionTime},
+			AcctInputOctets = %{query.InputOctets},
+			AcctInputGigawords = %{query.InputGigawords},
+			AcctInputPackets = %{query.InputPackets},
+			AcctOutputOctets = %{query.OutputOctets},
+			AcctOutputGigawords = %{query.OutputGigawords},
+			AcctOutputPackets = %{query.OutputPackets},
 			AcctStatusType = %{request.Acct-Status-Type}
 		WHERE
 			Username = %{request.User-Name}
 			AND AcctSessionID = %{request.Acct-Session-Id}
 			AND NASIPAddress = %{request.NAS-IP-Address}
+			AND PeriodKey = %{query.PeriodKey}
 	';
 
 	$config->{'accounting_stop_query'} = '
 		UPDATE
 			@TP@accounting
 		SET
-			AcctSessionTime = %{request.Acct-Session-Time},
-			AcctInputOctets = %{request.Acct-Input-Octets},
-			AcctInputGigawords = %{request.Acct-Input-Gigawords},
-			AcctInputPackets = %{request.Acct-Input-Packets},
-			AcctOutputOctets = %{request.Acct-Output-Octets},
-			AcctOutputGigawords = %{request.Acct-Output-Gigawords},
-			AcctOutputPackets = %{request.Acct-Output-Packets},
+			AcctSessionTime = %{query.SessionTime},
+			AcctInputOctets = %{query.InputOctets},
+			AcctInputGigawords = %{query.InputGigawords},
+			AcctInputPackets = %{query.InputPackets},
+			AcctOutputOctets = %{query.OutputOctets},
+			AcctOutputGigawords = %{query.OutputGigawords},
+			AcctOutputPackets = %{query.OutputPackets}
+		WHERE
+			Username = %{request.User-Name}
+			AND AcctSessionID = %{request.Acct-Session-Id}
+			AND NASIPAddress = %{request.NAS-IP-Address}
+			AND PeriodKey = %{query.PeriodKey}
+	';
+
+	$config->{'accounting_stop_status_query'} = '
+		UPDATE
+			@TP@accounting
+		SET
 			AcctStatusType = %{request.Acct-Status-Type},
 			AcctTerminateCause = %{request.Acct-Terminate-Cause}
 		WHERE
@@ -165,7 +202,7 @@ sub init
 			@TP@accounting
 		WHERE
 			Username = %{request.User-Name}
-			AND EventTimestamp >= %{query.From}
+			AND PeriodKey = %{query.PeriodKey}
 	';
 
 	# Setup SQL queries
@@ -180,6 +217,15 @@ sub init
 				$config->{'accounting_start_query'} = $scfg->{'mod_accounting_sql'}->{'accounting_start_query'};
 			}
 		}
+		if (defined($scfg->{'mod_accounting_sql'}->{'accounting_update_get_records_query'}) &&
+				$scfg->{'mod_accounting_sql'}->{'accounting_update_get_records_query'} ne "") {
+			if (ref($scfg->{'mod_accounting_sql'}->{'accounting_update_get_records_query'}) eq "ARRAY") {
+				$config->{'accounting_update_get_records_query'} = join(' ',
+						@{$scfg->{'mod_accounting_sql'}->{'accounting_update_get_records_query'}});
+			} else {
+				$config->{'accounting_update_get_records_query'} = $scfg->{'mod_accounting_sql'}->{'accounting_update_get_records_query'};
+			}
+		}
 		if (defined($scfg->{'mod_accounting_sql'}->{'accounting_update_query'}) &&
 				$scfg->{'mod_accounting_sql'}->{'accounting_update_query'} ne "") {
 			if (ref($scfg->{'mod_accounting_sql'}->{'accounting_update_query'}) eq "ARRAY") {
@@ -198,6 +244,15 @@ sub init
 				$config->{'accounting_stop_query'} = $scfg->{'mod_accounting_sql'}->{'accounting_stop_query'};
 			}
 		}
+		if (defined($scfg->{'mod_accounting_sql'}->{'accounting_stop_status_query'}) &&
+				$scfg->{'mod_accounting_sql'}->{'accounting_stop_status_query'} ne "") {
+			if (ref($scfg->{'mod_accounting_sql'}->{'accounting_stop_status_query'}) eq "ARRAY") {
+				$config->{'accounting_stop_status_query'} = join(' ',
+						@{$scfg->{'mod_accounting_sql'}->{'accounting_stop_status_query'}});
+			} else {
+				$config->{'accounting_stop_status_query'} = $scfg->{'mod_accounting_sql'}->{'accounting_stop_status_query'};
+			}
+		}
 		if (defined($scfg->{'mod_accounting_sql'}->{'accounting_usage_query'}) &&
 				$scfg->{'mod_accounting_sql'}->{'accounting_usage_query'} ne "") {
 			if (ref($scfg->{'mod_accounting_sql'}->{'accounting_usage_query'}) eq "ARRAY") {
@@ -214,7 +269,7 @@ sub init
 # Function to get radius user data usage
 sub getUsage
 {
-	my ($server,$user,$packet,$month) = @_;
+	my ($server,$user,$packet) = @_;
 
 	# Build template
 	my $template;
@@ -222,13 +277,13 @@ sub getUsage
 		$template->{'request'}->{$attr} = $packet->rawattr($attr)
 	}
 	$template->{'user'} = $user;
-	# Query parameters
-	$template->{'query'}->{'From'} = $month;
+
+	# Current PeriodKey
+	my $now = DateTime->now;
+	$template->{'query'}->{'PeriodKey'} = $now->strftime("%Y-%m");
 
 	# Replace template entries
-	my ($queryString, @params) = templateReplace($config->{'accounting_usage_query'},$template);
-	# Add month to our params
-	my @dbDoParams = ($queryString, @params);
+	my (@dbDoParams) = templateReplace($config->{'accounting_usage_query'},$template);
 
 	# Fetch data
 	my $sth = DBSelect(@dbDoParams);
@@ -296,20 +351,30 @@ sub acct_log
 {
 	my ($server,$user,$packet) = @_;
 
-
 	# Build template
 	my $template;
 	foreach my $attr ($packet->attributes) {
-		$template->{'request'}->{$attr} = $packet->rawattr($attr)
+		$template->{'request'}->{$attr} = $packet->rawattr($attr);
 	}
 	# Fix event timestamp
 	$template->{'request'}->{'Timestamp'} = $user->{'_Internal'}->{'Timestamp'};
+
 	# Add user
 	$template->{'user'} = $user;
 
+	# Current PeriodKey
+	my $now = DateTime->now;
+	my $periodKey = $now->strftime("%Y-%m");
+
+	# For our queries
+	$template->{'query'}->{'PeriodKey'} = $periodKey;
 
+	#
+	# S T A R T   P A C K E T
+	#
 
 	if ($packet->attr('Acct-Status-Type') eq "Start") {
+
 		# Replace template entries
 		my @dbDoParams = templateReplace($config->{'accounting_start_query'},$template);
 
@@ -321,24 +386,197 @@ sub acct_log
 			return MOD_RES_NACK;
 		}
 
+	#
+	# U P D A T E   P A C K E T
+	#
+
 	} elsif ($packet->attr('Acct-Status-Type') eq "Alive") {
 		# Replace template entries
-		my @dbDoParams = templateReplace($config->{'accounting_update_query'},$template);
+		my @dbDoParams = templateReplace($config->{'accounting_update_get_records_query'},$template);
 
-		# Update database
-		my $sth = DBDo(@dbDoParams);
+		# Fetch previous records of the same session
+		my $sth = DBSelect(@dbDoParams);
 		if (!$sth) {
-			$server->log(LOG_ERR,"[MOD_ACCOUNTING_SQL] Failed to update accounting ALIVE record: ".
-					awitpt::db::dblayer::Error());
-			return MOD_RES_NACK;
+			$server->log(LOG_ERR,"[MOD_ACCOUNTING_SQL] Database query failed: ".awitpt::db::dblayer::Error());
+			return;
+		}
+
+		# Convert session total gigawords/octets into bytes
+		my $totalInputBytes = Math::BigInt->new();
+		$totalInputBytes->badd($template->{'request'}->{'Acct-Input-Gigawords'})->bmul(UINT_MAX);
+		$totalInputBytes->badd($template->{'request'}->{'Acct-Input-Octets'});
+		my $totalOutputBytes = Math::BigInt->new();
+		$totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Gigawords'})->bmul(UINT_MAX);
+		$totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Octets'});
+
+		# Loop through previous records and subtract them from our session totals
+		my $startNewPeriod = 0;
+		$template->{'query'}->{'InputPackets'} = $template->{'request'}->{'Acct-Input-Packets'};
+		$template->{'query'}->{'OutputPackets'} = $template->{'request'}->{'Acct-Output-Packets'};
+		$template->{'query'}->{'SessionTime'} = $template->{'request'}->{'Acct-Session-Time'};
+		while (my $sessionPart = $sth->fetchrow_hashref()) {
+			$sessionPart = hashifyLCtoMC(
+				$sessionPart,
+				qw(InputOctets InputPackets OutputOctets OutputPackets InputGigawords OutputGigawords SessionTime PeriodKey)
+			);
+
+			# Convert this session usage to bytes
+			my $sessionInputBytes = Math::BigInt->new();
+			$sessionInputBytes->badd($sessionPart->{'InputGigawods'})->bmul(UINT_MAX);
+			$sessionInputBytes->badd($sessionPart->{'InputOctets'});
+			my $sessionOutputBytes = Math::BigInt->new();
+			$sessionOutputBytes->badd($sessionPart->{'OutputGigawods'})->bmul(UINT_MAX);
+			$sessionOutputBytes->badd($sessionPart->{'OutputOctets'});
+
+			# Check if this record is from an earlier period
+			$startNewPeriod = 0;
+			if (defined($sessionPart->{'PeriodKey'}) && $sessionPart->{'PeriodKey'} ne $periodKey) {
+
+				# Subtract from our total
+				$totalInputBytes->bsub($sessionInputBytes);
+				$totalOutputBytes->bsub($sessionOutputBytes);
+
+				# Subtract other usage
+				if (defined($sessionPart->{'InputPackets'}) && $sessionPart->{'InputPackets'} > 0) {
+					$template->{'query'}->{'InputPackets'} -= $sessionPart->{'InputPackets'};
+				}
+				if (defined($sessionPart->{'OutputPackets'}) && $sessionPart->{'OutputPackets'} > 0) {
+					$template->{'query'}->{'OutputPackets'} -= $sessionPart->{'OutputPackets'};
+				}
+				if (defined($sessionPart->{'SessionTime'}) && $sessionPart->{'SessionTime'} > 0) {
+					$template->{'query'}->{'SessionTime'} -= $sessionPart->{'SessionTime'};
+				}
+
+				# We need to continue this session in a new entry
+				$startNewPeriod = 1;
+			}
 		}
 
+		# Re-calculate
+		my ($inputGigawordsStr,$inputOctetsStr) = $totalInputBytes->bdiv(UINT_MAX);
+		my ($outputGigawordsStr,$outputOctetsStr) = $totalOutputBytes->bdiv(UINT_MAX);
+
+		# Conversion to strings
+		$template->{'query'}->{'InputGigawords'} = $inputGigawordsStr->bstr();
+		$template->{'query'}->{'InputOctets'} = $inputOctetsStr->bstr();
+		$template->{'query'}->{'OutputGigawords'} = $outputGigawordsStr->bstr();
+		$template->{'query'}->{'OutputOctets'} = $outputOctetsStr->bstr();
+
+		# Check if we doing an update
+		if ($startNewPeriod == 0) {
+			# Replace template entries
+			@dbDoParams = templateReplace($config->{'accounting_update_query'},$template);
+
+			# Update database
+			my $sth = DBDo(@dbDoParams);
+			if (!$sth) {
+				$server->log(LOG_ERR,"[MOD_ACCOUNTING_SQL] Failed to update accounting ALIVE record: ".
+						awitpt::db::dblayer::Error());
+				return MOD_RES_NACK;
+			}
+		# Else do a start record to continue session
+		} else {
+			# Replace template entries
+			my @dbDoParams = templateReplace($config->{'accounting_start_query'},$template);
+
+			# Insert into database
+			my $sth = DBDo(@dbDoParams);
+			if (!$sth) {
+				$server->log(LOG_ERR,"[MOD_ACCOUNTING_SQL] Failed to insert accounting START record: ".
+						awitpt::db::dblayer::Error());
+				return MOD_RES_NACK;
+			}
+			$startNewPeriod = 0;
+		}
+
+	#
+	# S T O P   P A C K E T
+	#
+
 	} elsif ($packet->attr('Acct-Status-Type') eq "Stop") {
+
 		# Replace template entries
-		my @dbDoParams = templateReplace($config->{'accounting_stop_query'},$template);
+		my @dbDoParams = templateReplace($config->{'accounting_update_get_records_query'},$template);
 
-		# Update database
-		my $sth = DBDo(@dbDoParams);
+		# Fetch data
+		my $sth = DBSelect(@dbDoParams);
+		if (!$sth) {
+			$server->log(LOG_ERR,"[MOD_ACCOUNTING_SQL] Database query failed: ".awitpt::db::dblayer::Error());
+			return MOD_RES_NACK;
+		}
+
+		# Convert session total gigawords/octets into bytes
+		my $totalInputBytes = Math::BigInt->new();
+		$totalInputBytes->badd($template->{'request'}->{'Acct-Input-Gigawords'})->bmul(UINT_MAX);
+		$totalInputBytes->badd($template->{'request'}->{'Acct-Input-Octets'});
+		my $totalOutputBytes = Math::BigInt->new();
+		$totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Gigawords'})->bmul(UINT_MAX);
+		$totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Octets'});
+
+		# Loop through records and subtract from our totals if needed
+		$template->{'query'}->{'InputPackets'} = $template->{'request'}->{'Acct-Input-Packets'};
+		$template->{'query'}->{'OutputPackets'} = $template->{'request'}->{'Acct-Output-Packets'};
+		$template->{'query'}->{'SessionTime'} = $template->{'request'}->{'Acct-Session-Time'};
+		while (my $sessionPart = $sth->fetchrow_hashref()) {
+			$sessionPart = hashifyLCtoMC(
+				$sessionPart,
+				qw(InputOctets InputPackets OutputOctets OutputPackets InputGigawords OutputGigawords SessionTime PeriodKey)
+			);
+
+			# Convert this session usage to bytes
+			my $sessionInputBytes = Math::BigInt->new();
+			$sessionInputBytes->badd($sessionPart->{'InputGigawods'})->bmul(UINT_MAX);
+			$sessionInputBytes->badd($sessionPart->{'InputOctets'});
+			my $sessionOutputBytes = Math::BigInt->new();
+			$sessionOutputBytes->badd($sessionPart->{'OutputGigawods'})->bmul(UINT_MAX);
+			$sessionOutputBytes->badd($sessionPart->{'OutputOctets'});
+
+			# Subtract this period/session usage from total
+			if (defined($sessionPart->{'PeriodKey'}) && $sessionPart->{'PeriodKey'} ne $periodKey) {
+
+				# Subtract from our total
+				$totalInputBytes->bsub($sessionInputBytes);
+				$totalOutputBytes->bsub($sessionInputBytes);
+
+				# Subtract other usage
+				if (defined($sessionPart->{'InputPackets'}) && $sessionPart->{'InputPackets'} > 0) {
+					$template->{'query'}->{'InputPackets'} -= $sessionPart->{'InputPackets'};
+				}
+				if (defined($sessionPart->{'OutputPackets'}) && $sessionPart->{'OutputPackets'} > 0) {
+					$template->{'query'}->{'OutputPackets'} -= $sessionPart->{'OutputPackets'};
+				}
+				if (defined($sessionPart->{'SessionTime'}) && $sessionPart->{'SessionTime'} > 0) {
+					$template->{'query'}->{'SessionTime'} -= $sessionPart->{'SessionTime'};
+				}
+			}
+		}
+		DBFreeRes($sth);
+
+		# Re-calculate
+		my ($inputGigawordsStr,$inputOctetsStr) = $totalInputBytes->bdiv(UINT_MAX);
+		my ($outputGigawordsStr,$outputOctetsStr) = $totalOutputBytes->bdiv(UINT_MAX);
+
+		# Conversion to strings
+		$template->{'query'}->{'InputGigawords'} = $inputGigawordsStr->bstr();
+		$template->{'query'}->{'InputOctets'} = $inputOctetsStr->bstr();
+		$template->{'query'}->{'OutputGigawords'} = $outputGigawordsStr->bstr();
+		$template->{'query'}->{'OutputOctets'} = $outputOctetsStr->bstr();
+
+		# Replace template entries
+		@dbDoParams = templateReplace($config->{'accounting_stop_query'},$template);
+
+		# Update database (totals)
+		$sth = DBDo(@dbDoParams);
+		if (!$sth) {
+			$server->log(LOG_ERR,"[MOD_ACCOUNTING_SQL] Failed to update accounting STOP record: ".awitpt::db::dblayer::Error());
+			return MOD_RES_NACK;
+		}
+
+		# Replace template entries
+		@dbDoParams = templateReplace($config->{'accounting_stop_status_query'},$template);
+
+		# Update database (status)
+		$sth = DBDo(@dbDoParams);
 		if (!$sth) {
 			$server->log(LOG_ERR,"[MOD_ACCOUNTING_SQL] Failed to update accounting STOP record: ".awitpt::db::dblayer::Error());
 			return MOD_RES_NACK;
@@ -353,12 +591,12 @@ sub acct_log
 sub cleanup
 {
 	my ($server) = @_;
-	my ($prevYear,$prevMonth);
 
 	# The datetime now..
 	my $now = DateTime->now;
 
 	# If this is a new year
+	my ($prevYear,$prevMonth);
 	if ($now->month == 1) {
 		$prevYear = $now->year - 1;
 		$prevMonth = 12;
@@ -369,8 +607,9 @@ sub cleanup
 
 	# New datetime
 	my $lastMonth = DateTime->new( year => $prevYear, month => $prevMonth, day => 1 );
+	my $periodKey = $lastMonth->strftime("%Y-%m");
 
-	# Update totals for last month
+	# Select totals for last month
 	my $sth = DBSelect('
 		SELECT
 			Username,
@@ -382,11 +621,11 @@ sub cleanup
 		FROM
 			@TP@accounting
 		WHERE
-			EventTimestamp > ?
+			PeriodKey = ?
 		GROUP BY
 			Username
 		',
-		$lastMonth->ymd
+		$periodKey
 	);
 
 	if (!$sth) {
@@ -396,67 +635,87 @@ sub cleanup
 	}
 
 	# Set blank array
-	my @allRecords = ();
+	my @allRecords;
 
-	my $i = 0;
 	# Load items into array
+	my $index = 0;
 	while (my $usageTotals = $sth->fetchrow_hashref()) {
 		$usageTotals = hashifyLCtoMC(
 			$usageTotals,
 			qw(Username AcctSessionTime AcctInputOctets AcctInputGigawords AcctOutputOctets AcctOutputGigawords)
 		);
 
-		# Set array blank
-		my @recordRow = ();
-
 		# Set array items
-		@recordRow = (
-			$usageTotals->{'Username'},
-			$lastMonth->ymd,
-			$usageTotals->{'AcctSessionTime'},
-			$usageTotals->{'AcctInputOctets'},
-			$usageTotals->{'AcctInputGigawords'},
-			$usageTotals->{'AcctOutputOctets'},
-			$usageTotals->{'AcctOutputGigawords'}
-		);
-
-		# Add record ontp @allRecords
-		@{$allRecords[$i]} = @recordRow;
-
-		# Increate array size
-		$i++;
+		$allRecords[$index] = {
+			Username => $usageTotals->{'Username'},
+			PeriodKey => $lastMonth->ymd,
+			SessionTime => $usageTotals->{'AcctSessionTime'},
+			InputOctets => $usageTotals->{'AcctInputOctets'},
+			InputGigawords => $usageTotals->{'AcctInputGigawords'},
+			OutputOctets => $usageTotals->{'AcctOutputOctets'},
+			OutputGigawords => $usageTotals->{'AcctOutputGigawords'}
+		};
+
+		# Increase size
+		$index++;
 	}
 
 	# Begin transaction
 	DBBegin();
 
-	my @dbDoParams = ();
-	my $count = length(@allRecords);
-
 	# Update totals for last month
-	for ($i = 0; $i < $count; $i++) {
+	if ($index > 0) {
+
+		# Delete duplicate records
+		my @dbDoParams;
 		@dbDoParams = ('
-			INSERT INTO
+			DELETE FROM
 				@TP@accounting_summary
-			(
-				Username,
-				PeriodKey,
-				AcctSessionTime,
-				AcctInputOctets,
-				AcctInputGigawords,
-				AcctOutputOctets,
-				AcctOutputGigawords
-			)
-			VALUES
-				(?,?,?,?,?,?,?)
-			',
-			@{$allRecords[$i]}
+			WHERE
+				PeriodKey = ?',
+			$lastMonth->ymd
 		);
 
 		if ($sth) {
 			# Do query
 			$sth = DBDo(@dbDoParams);
 		}
+
+		my @insertArray;
+		for (my $i = 0; $i < $index; $i++) {
+			@insertArray = (
+				$allRecords[$i]->{'Username'},
+				$allRecords[$i]->{'PeriodKey'},
+				$allRecords[$i]->{'SessionTime'},
+				$allRecords[$i]->{'InputOctets'},
+				$allRecords[$i]->{'InputGigawords'},
+				$allRecords[$i]->{'OutputOctets'},
+				$allRecords[$i]->{'OutputGigawords'}
+			);
+
+			@dbDoParams = ('
+				INSERT INTO
+					@TP@accounting_summary
+				(
+					Username,
+					PeriodKey,
+					AcctSessionTime,
+					AcctInputOctets,
+					AcctInputGigawords,
+					AcctOutputOctets,
+					AcctOutputGigawords
+				)
+				VALUES
+					(?,?,?,?,?,?,?)
+				',
+				@insertArray
+			);
+
+			if ($sth) {
+				# Do query
+				$sth = DBDo(@dbDoParams);
+			}
+		}
 	}
 
 	# Rollback with error if failed
diff --git a/smradius/modules/features/mod_feature_capping.pm b/smradius/modules/features/mod_feature_capping.pm
index 90fe2954..258ebd9d 100644
--- a/smradius/modules/features/mod_feature_capping.pm
+++ b/smradius/modules/features/mod_feature_capping.pm
@@ -118,14 +118,13 @@ sub post_auth_hook
 
 	# Get the users' usage
 	my $accountingUsage;
-	my $month = DateTime->from_epoch( epoch => $user->{'_Internal'}->{'Timestamp-Unix'} )->strftime('%Y-%m');
 	# Loop with plugins to find anyting supporting getting of usage
 	foreach my $module (@{$server->{'module_list'}}) {
 		# Do we have the correct plugin?
 		if ($module->{'Accounting_getUsage'}) {
 			$server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Found plugin: '".$module->{'Name'}."'");
 			# Fetch users session uptime & bandwidth used
-			my $res = $module->{'Accounting_getUsage'}($server,$user,$packet,$month);
+			my $res = $module->{'Accounting_getUsage'}($server,$user,$packet);
 			if (!defined($res)) {
 				$server->log(LOG_ERR,"[MOD_FEATURE_CAPPING] No usage data found for user '".$packet->attr('User-Name')."'");
 				return MOD_RES_SKIP;
@@ -318,7 +317,7 @@ sub post_acct_hook
 		if ($module->{'Accounting_getUsage'}) {
 			$server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Found plugin: '".$module->{'Name'}."'");
 			# Fetch users session uptime & bandwidth used
-			my $res = $module->{'Accounting_getUsage'}($server,$user,$packet,$month);
+			my $res = $module->{'Accounting_getUsage'}($server,$user,$packet);
 			if (!defined($res)) {
 				$server->log(LOG_ERR,"[MOD_FEATURE_CAPPING] No usage data found for user '".$packet->attr('User-Name')."'");
 				return MOD_RES_SKIP;
diff --git a/smradius/modules/system/mod_config_sql_topups.pm b/smradius/modules/system/mod_config_sql_topups.pm
index 3d8b3d4f..31f665f1 100644
--- a/smradius/modules/system/mod_config_sql_topups.pm
+++ b/smradius/modules/system/mod_config_sql_topups.pm
@@ -301,6 +301,7 @@ sub cleanup
 		$prevMonth = $now->month - 1;
 	}
 	my $lastMonth = DateTime->new( year => $prevYear, month => $prevMonth, day => 1 );
+	my $prevPeriodKey = $lastMonth->strftime("%Y-%m");
 
 	# Get begin date of next month
 	my ($folYear,$folMonth);
@@ -335,12 +336,12 @@ sub cleanup
 			FROM
 				@TP@accounting
 			WHERE
-				EventTimestamp > ?
+				PeriodKey = ?
 				AND Username = ?
 			GROUP BY
 				Username
 			',
-			$lastMonth,$userName
+			$prevPeriodKey,$userName
 		);
 
 		if (!$sth) {
@@ -428,7 +429,6 @@ sub cleanup
 
 
 		# Get users topups that are still valid from topups_summary, must not be depleted
-		my $prevPeriodKey = $lastMonth->strftime("%Y-%m");
 		$sth = DBSelect('
 			SELECT
 				@TP@topups_summary.TopupID,
@@ -783,7 +783,24 @@ sub cleanup
 
 		# Loop through summary topups
 		foreach my $summaryTopup (@summaryTopups) {
-			# Set users depleted topups
+
+			# Delete any previous record of this summary
+			$sth = DBDo('
+				DELETE FROM
+					@TP@topups_summary
+				WHERE
+					TopupID = ?
+					AND PeriodKey = ?',
+				$summaryTopup->{'ID'}, $periodKey
+			);
+
+			if (!$sth) {
+				$server->log(LOG_ERR,"[MOD_CONFIG_SQL_TOPUPS] Cleanup => Failed to delete previous record: ".
+						awitpt::db::dblayer::Error());
+				goto FAIL_ROLLBACK;
+			}
+
+			# Insert topup summary
 			$sth = DBDo('
 				INSERT INTO
 					@TP@topups_summary (TopupID,PeriodKey,Balance)
diff --git a/smradiusd.conf b/smradiusd.conf
index 1719bad6..7d951de2 100644
--- a/smradiusd.conf
+++ b/smradiusd.conf
@@ -235,7 +235,8 @@ accounting_start_query=<<EOT
 		AcctStatusType,
 		NASIdentifier,
 		NASIPAddress,
-		AcctDelayTime
+		AcctDelayTime,
+		PeriodKey
 	)
 	VALUES
 	(
@@ -254,39 +255,74 @@ accounting_start_query=<<EOT
 		%{request.Acct-Status-Type},
 		%{request.NAS-Identifier},
 		%{request.NAS-IP-Address},
-		%{request.Acct-Delay-Time}
+		%{request.Acct-Delay-Time},
+		%{query.PeriodKey}
 	)
 EOT
 
+accounting_update_get_records_query=<<EOT
+	SELECT
+		SUM(AcctInputOctets) AS InputOctets,
+		SUM(AcctInputPackets) AS InputPackets,
+		SUM(AcctOutputOctets) AS OutputOctets,
+		SUM(AcctOutputPackets) AS OutputPackets,
+		SUM(AcctInputGigawords) AS InputGigawords,
+		SUM(AcctOutputGigawords) AS OutputGigawords,
+		SUM(AcctSessionTime) AS SessionTime,
+		PeriodKey
+	FROM
+		@TP@accounting
+	WHERE
+		Username = %{request.User-Name}
+		AND AcctSessionID = %{request.Acct-Session-Id}
+		AND NASIPAddress = %{request.NAS-IP-Address}
+	GROUP BY
+		PeriodKey
+	ORDER BY
+		ID ASC
+EOT
+
 accounting_update_query=<<EOT
 	UPDATE
 		@TP@accounting
 	SET
-		AcctSessionTime = %{request.Acct-Session-Time},
-		AcctInputOctets = %{request.Acct-Input-Octets},
-		AcctInputGigawords = %{request.Acct-Input-Gigawords},
-		AcctInputPackets = %{request.Acct-Input-Packets},
-		AcctOutputOctets = %{request.Acct-Output-Octets},
-		AcctOutputGigawords = %{request.Acct-Output-Gigawords},
-		AcctOutputPackets = %{request.Acct-Output-Packets},
+		AcctSessionTime = %{query.SessionTime},
+		AcctInputOctets = %{query.InputOctets},
+		AcctInputGigawords = %{query.InputGigawords},
+		AcctInputPackets = %{query.InputPackets},
+		AcctOutputOctets = %{query.OutputOctets},
+		AcctOutputGigawords = %{query.OutputGigawords},
+		AcctOutputPackets = %{query.OutputPackets},
 		AcctStatusType = %{request.Acct-Status-Type}
 	WHERE
 		Username = %{request.User-Name}
 		AND AcctSessionID = %{request.Acct-Session-Id}
 		AND NASIPAddress = %{request.NAS-IP-Address}
+		AND PeriodKey = %{query.PeriodKey}
 EOT
 
 accounting_stop_query=<<EOT
 	UPDATE
 		@TP@accounting
 	SET
-		AcctSessionTime = %{request.Acct-Session-Time},
-		AcctInputOctets = %{request.Acct-Input-Octets},
-		AcctInputGigawords = %{request.Acct-Input-Gigawords},
-		AcctInputPackets = %{request.Acct-Input-Packets},
-		AcctOutputOctets = %{request.Acct-Output-Octets},
-		AcctOutputGigawords = %{request.Acct-Output-Gigawords},
-		AcctOutputPackets = %{request.Acct-Output-Packets},
+		AcctSessionTime = %{query.SessionTime},
+		AcctInputOctets = %{query.InputOctets},
+		AcctInputGigawords = %{query.InputGigawords},
+		AcctInputPackets = %{query.InputPackets},
+		AcctOutputOctets = %{query.OutputOctets},
+		AcctOutputGigawords = %{query.OutputGigawords},
+		AcctOutputPackets = %{query.OutputPackets}
+	WHERE
+		Username = %{request.User-Name}
+		AND AcctSessionID = %{request.Acct-Session-Id}
+		AND NASIPAddress = %{request.NAS-IP-Address}
+		AND PeriodKey = %{query.PeriodKey}
+EOT
+
+accounting_stop_status_query=<<EOT
+	UPDATE
+		@TP@accounting
+	SET
 		AcctStatusType = %{request.Acct-Status-Type},
 		AcctTerminateCause = %{request.Acct-Terminate-Cause}
 	WHERE
@@ -306,7 +342,7 @@ accounting_usage_query=<<EOT
 		@TP@accounting
 	WHERE
 		Username = %{request.User-Name}
-		AND EventTimestamp >= %{query.From}
+		AND PeriodKey = %{query.PeriodKey}
 EOT
 
 
-- 
GitLab