package RTI::State; use strict; use warnings; use 5.010; use DateTime; use Carp; use YAML::XS qw/ LoadFile DumpFile /; $YAML::XS::QuoteNumericStrings = 0; use RTI::Util qw/ ymd_to_DateTime /; my $file = ''; sub new { my $class; ( $class, $file ) = @_; my $self = { lastinvoice => 0, }; if ( -e $file ) { $self = LoadFile($file) or die "Unable to load state: $!"; $self->{lastinvoice} ||= 0; while ( my ( $id, $invoice ) = each %{ $self->{invoice} } ) { $self->{lastinvoice} = $id if $self->{lastinvoice} < $id; $invoice->{id} = $id; $invoice->{$_} = ymd_to_DateTime( $invoice->{$_} ) for qw/ invdate start end /; } foreach my $custid (keys %{ $self->{payment} || {} }) { foreach my $payment (@{ $self->{payment}->{$custid} || [] }) { $payment->{date} = ymd_to_DateTime( $payment->{date} ) if $payment->{date}; } } } bless $self, $class; die "Need to pass filename to new: $!" unless $file; return $self; } sub next_invoice_id { my ($self) = @_; return $self->{lastinvoice} + 1; } sub add_invoice { my ( $self, $invoice ) = @_; my $id = $invoice->{id} || $self->next_invoice_id; croak "Can't add duplicate invoice $id\n" if exists $self->{invoice}->{$id}; $invoice->{id} ||= $id; $invoice->{invdate} ||= DateTime->now( time_zone => 'local' ), $self->{lastinvoice} = $id if $self->{lastinvoice} < $id; $self->{invoice}->{$id} = $invoice; delete $self->{_tables}; return $self->{lastinvoice}; } sub get_invoice { my ( $self, $id ) = @_; return $self->{invoice}->{$id}; } sub last_invoice { my ( $self, $custid ) = @_; if ( !$self->{_table}->{last_invoice} ) { my $invoices = $self->{invoice}; foreach my $id ( sort { $a <=> $b } keys %{$invoices} ) { my $inv = $invoices->{$id}; next unless $inv->{custid}; $self->{_table}->{last_invoice}->{ $inv->{custid} } = $inv; } } return $self->{_table}->{last_invoice}->{$custid}; } sub txn_is_invoiced { my ( $self, $txn ) = @_; if ( !$self->{_table}->{txn} ) { my $invoices = $self->{invoice}; foreach my $id ( sort { $a <=> $b } keys %{$invoices} ) { my $inv = $invoices->{$id}; foreach my $t ( @{ $inv->{transactions} } ) { $self->{_table}->{txn}->{$t} = 1; } } } return $self->{_table}->{txn}->{$txn}; } sub unpaid_invoices { my ( $self, $custid ) = @_; $self->_match_payments; return defined $custid ? $self->{_table}->{unpaid}->{$custid} : $self->{_table}->{unpaid}; } sub credits { my ( $self, $custid ) = @_; $self->_match_payments; return defined $custid ? $self->{_table}->{credit}->{$custid} : $self->{_table}->{credit}; } sub save { my ($self) = @_; delete $self->{_table}; delete $self->{lastinvoice}; foreach my $invoice ( values %{ $self->{invoice} } ) { delete $invoice->{$_} for qw/ id from to info logo projects expenses discount hours organization /; foreach my $k ( keys %{$invoice} ) { my $v = $invoice->{$k}; if ( defined $v && length $v ) { if ( ref $v eq 'DateTime' ) { $invoice->{$k} = $v->ymd; } } else { delete $invoice->{$k}; } } } foreach my $custid (keys %{ $self->{payment} || {} }) { foreach my $payment (@{ $self->{payment}->{$custid} || [] }) { $payment->{date} = $payment->{date}->ymd if ref $payment->{date} eq 'DateTime'; } } DumpFile( $file, {%$self} ) or die "Unable to save state: $!"; } sub _match_payments { my ($self) = @_; return if $self->{_table}{credit} && $self->{_table}{unpaid}; my $invoices = $self->{invoice}; my %owes = map { $_ => $invoices->{$_}->{total} } keys %{$invoices}; my %credit; foreach my $custid ( keys %{ $self->{payment} } ) { $credit{$custid} = 0; my $payments = $self->{payment}->{$custid}; foreach my $p ( @{$payments} ) { my $paid = $p->{paid}; $p->{invoices} ||= []; foreach my $id ( @{ $p->{invoices} } ) { $owes{$id} ||= 0; if ( $owes{$id} == $paid ) { $owes{$id} = 0; $paid = 0; } elsif ( $owes{$id} > $paid ) { $owes{$id} -= $paid; $paid = 0; } elsif ( $owes{$id} < $paid ) { $paid -= $owes{$id}; $owes{$id} = 0; } } $credit{$custid} += $paid; } delete $credit{$custid} unless $credit{$custid}; } foreach my $id ( sort { $a <=> $b } keys %owes ) { my $i = $invoices->{$id}; my $custid = $i->{custid} or next; my $owes = sprintf "%0.2f", $owes{$id} || 0; my $paid = sprintf "%0.2f", $credit{$custid} || 0; if ( $owes == $paid ) { $owes = 0; $paid = 0; } elsif ( $owes > $paid ) { $owes -= $paid; $paid = 0; } elsif ( $owes < $paid ) { $paid -= $owes; $owes = 0; } $self->{_table}{unpaid}{$custid}{$id} = $owes if $owes; if ($paid) { $credit{$custid} = $paid; } elsif ( exists $credit{$custid} ) { delete $credit{$custid}; } } $self->{_table}{credit} = \%credit; } 1;