+=head1 METHODS
+
+=over 4
+
+=item init_projection
+
+Sets up for future projection of all observables on the report. Currently
+this is limited to 'cust_bill_pkg'.
+
+=cut
+
+sub init_projection {
+ # this is weird special case stuff--some redesign may be needed
+ # to use it for anything else
+ my $self = shift;
+
+ if ( driver_name ne 'Pg' ) {
+ # also database-specific for now
+ die "projection reports not supported on this platform";
+ }
+
+ my %items = map {$_ => 1} @{ $self->{items} };
+ if ($items{'cust_bill_pkg'}) {
+ my $dbh = dbh;
+ # v_ for 'virtual'
+ my @sql = (
+ # could use TEMPORARY TABLE but we're already transaction-protected
+ 'DROP TABLE IF EXISTS v_cust_bill_pkg',
+ 'CREATE TABLE v_cust_bill_pkg ' .
+ '(LIKE cust_bill_pkg,
+ usage numeric(10,2), _date integer, expire integer)',
+ # XXX this should be smart enough to take only the ones with
+ # sdate/edate overlapping the ROI, for performance
+ "INSERT INTO v_cust_bill_pkg (
+ SELECT cust_bill_pkg.*,
+ (SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
+ FROM cust_bill_pkg_detail
+ WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum),
+ cust_bill._date,
+ cust_pkg.expire
+ FROM cust_bill_pkg $cust_bill_pkg_join
+ )",
+ );
+ foreach my $sql (@sql) {
+ warn "[init_projection] $sql\n" if $DEBUG;
+ $dbh->do($sql) or die $dbh->errstr;
+ }
+ }
+}
+
+=item extend_projection START END
+
+Generates data for the next period of projection. This will be called
+for sequential periods where the END of one equals the START of the next
+(with no gaps).
+
+=cut
+
+sub extend_projection {
+ my $self = shift;
+ my ($speriod, $eperiod) = @_;
+ my %items = map {$_ => 1} @{ $self->{items} };
+ if ($items{'cust_bill_pkg'}) {
+ # What we do here:
+ # Find all line items that end after the start of the period (and have
+ # recurring fees, and don't expire before they end). Choose the latest
+ # one for each package. If it ends before the end of the period, copy
+ # it forward by one billing period.
+ # Repeat this until the latest line item for each package no longer ends
+ # within the period. This is certain to happen in finitely many
+ # iterations as long as freq > 0.
+ # - Pg only, obviously.
+ # - Gives bad results if freq_override is used.
+ my @fields = ( FS::cust_bill_pkg->fields, qw( usage _date expire ) );
+ my $insert_fields = join(',', @fields);
+ my $add_freq = sub { # emulate FS::part_pkg::add_freq
+ my $field = shift;
+ "EXTRACT( EPOCH FROM TO_TIMESTAMP($field) + (CASE WHEN freq ~ E'\\\\D' ".
+ "THEN freq ELSE freq || 'mon' END)::INTERVAL) AS $field";
+ };
+ foreach (@fields) {
+ if ($_ eq 'edate') {
+ $_ = $add_freq->('edate');
+ }
+ elsif ($_ eq 'sdate') {
+ $_ = 'edate AS sdate'
+ }
+ elsif ($_ eq 'setup') {
+ $_ = '0 AS setup' #because recurring only
+ }
+ elsif ($_ eq '_date') {
+ $_ = $add_freq->('_date');
+ }
+ }
+ my $select_fields = join(',', @fields);
+ my $dbh = dbh;
+ my $sql =
+ # Subquery here because we need to DISTINCT the whole set, select the
+ # latest charge per pkgnum, and _then_ check edate < $eperiod
+ # and edate < expire.
+ "INSERT INTO v_cust_bill_pkg ($insert_fields)
+ SELECT $select_fields FROM (
+ SELECT DISTINCT ON (pkgnum) * FROM v_cust_bill_pkg
+ WHERE edate >= $speriod
+ AND recur > 0
+ AND freq IS NOT NULL
+ AND freq != '0'
+ ORDER BY pkgnum, edate DESC
+ ) AS v1
+ WHERE edate < $eperiod AND (edate < expire OR expire IS NULL)";
+ my $rows;
+ do {
+ warn "[extend_projection] $sql\n" if $DEBUG;
+ $rows = $dbh->do($sql) or die $dbh->errstr;
+ warn "[extend_projection] $rows rows\n" if $DEBUG;
+ } until $rows == 0;
+ }
+}
+
+=item _init_tower_pkg_cache
+
+Internal method: creates a temporary table relating pkgnums to towernums.
+A (pkgnum, towernum) record indicates that this package once had a
+svc_broadband service which, as of its last insert or replace_new history
+record, had a sectornum associated with that towernum.
+
+This is expensive, so it won't be done more than once an hour. Historical
+data about package churn shouldn't be changing in realtime anyway.
+
+=cut
+
+sub _init_tower_pkg_cache {
+ my $self = shift;
+ my $dbh = dbh;
+
+ my $current = $CACHE->get('tower_pkg_cache_update');
+ return if $current;
+
+ # XXX or should this be in the schema?
+ my $sql = "DROP TABLE IF EXISTS tower_pkg_cache";
+ $dbh->do($sql) or die $dbh->errstr;
+ $sql = "CREATE TABLE tower_pkg_cache (towernum int, pkgnum int)";
+ $dbh->do($sql) or die $dbh->errstr;
+
+ # assumptions:
+ # sectornums never get reused, or move from one tower to another
+ # all service history is intact
+ # svcnums never get reused (this would be bad)
+ # pkgnums NEVER get reused (this would be extremely bad)
+ $sql = "INSERT INTO tower_pkg_cache (
+ SELECT COALESCE(towernum,0), pkgnum
+ FROM ( SELECT DISTINCT pkgnum, svcnum FROM h_cust_svc ) AS pkgnum_svcnum
+ LEFT JOIN (
+ SELECT DISTINCT ON(svcnum) svcnum, sectornum
+ FROM h_svc_broadband
+ WHERE (history_action = 'replace_new'
+ OR history_action = 'replace_old')
+ ORDER BY svcnum ASC, history_date DESC
+ ) AS svcnum_sectornum USING (svcnum)
+ LEFT JOIN tower_sector USING (sectornum)
+ )";
+ $dbh->do($sql) or die $dbh->errstr;
+
+ $CACHE->set('tower_pkg_cache_update', 1, 3600);
+
+};
+