]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Merge branch 'master' of git.evergreen-ils.org:Evergreen into template-toolkit-opac
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / EGCatLoader / Account.pm
1 package OpenILS::WWW::EGCatLoader;
2 use strict; use warnings;
3 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
4 use OpenSRF::Utils::Logger qw/$logger/;
5 use OpenILS::Utils::CStoreEditor qw/:funcs/;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenILS::Application::AppUtils;
8 use OpenILS::Event;
9 use OpenSRF::Utils::JSON;
10 my $U = 'OpenILS::Application::AppUtils';
11
12 sub prepare_extended_user_info {
13     my $self = shift;
14
15     $self->ctx->{user} = $self->editor->retrieve_actor_user([
16         $self->ctx->{user}->id,
17         {
18             flesh => 1,
19             flesh_fields => {
20                 au => [qw/card home_ou addresses ident_type billing_address/]
21                 # ...
22             }
23         }
24     ]) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
25
26     return;
27 }
28
29 # context additions: 
30 #   user : au object, fleshed
31 sub load_myopac_prefs {
32     my $self = shift;
33     return $self->prepare_extended_user_info || Apache2::Const::OK;
34 }
35
36 sub load_myopac_prefs_notify {
37     my $self = shift;
38     my $e = $self->editor;
39
40     my $user_prefs = $self->fetch_optin_prefs;
41     $user_prefs = $self->update_optin_prefs($user_prefs)
42         if $self->cgi->request_method eq 'POST';
43
44     $self->ctx->{opt_in_settings} = $user_prefs; 
45
46     return Apache2::Const::OK;
47 }
48
49 sub fetch_optin_prefs {
50     my $self = shift;
51     my $e = $self->editor;
52
53     # fetch all of the opt-in settings the user has access to
54     # XXX: user's should in theory have options to opt-in to notices
55     # for remote locations, but that opens the door for a large
56     # set of generally un-used opt-ins.. needs discussion
57     my $opt_ins =  $U->simplereq(
58         'open-ils.actor',
59         'open-ils.actor.event_def.opt_in.settings.atomic',
60         $e->authtoken, $e->requestor->home_ou);
61
62     # fetch user setting values for each of the opt-in settings
63     my $user_set = $U->simplereq(
64         'open-ils.actor',
65         'open-ils.actor.patron.settings.retrieve',
66         $e->authtoken, 
67         $e->requestor->id, 
68         [map {$_->name} @$opt_ins]
69     );
70
71     return [map { {cust => $_, value => $user_set->{$_->name} } } @$opt_ins];
72 }
73
74 sub update_optin_prefs {
75     my $self = shift;
76     my $user_prefs = shift;
77     my $e = $self->editor;
78     my @settings = $self->cgi->param('setting');
79     my %newsets;
80
81     # apply now-true settings
82     for my $applied (@settings) {
83         # see if setting is already applied to this user
84         next if grep { $_->{cust}->name eq $applied and $_->{value} } @$user_prefs;
85         $newsets{$applied} = OpenSRF::Utils::JSON->true;
86     }
87
88     # remove now-false settings
89     for my $pref (grep { $_->{value} } @$user_prefs) {
90         $newsets{$pref->{cust}->name} = undef 
91             unless grep { $_ eq $pref->{cust}->name } @settings;
92     }
93
94     $U->simplereq(
95         'open-ils.actor',
96         'open-ils.actor.patron.settings.update',
97         $e->authtoken, $e->requestor->id, \%newsets);
98
99     # update the local prefs to match reality
100     for my $pref (@$user_prefs) {
101         $pref->{value} = $newsets{$pref->{cust}->name} 
102             if exists $newsets{$pref->{cust}->name};
103     }
104
105     return $user_prefs;
106 }
107
108 sub load_myopac_prefs_settings {
109     my $self = shift;
110     return $self->prepare_extended_user_info || Apache2::Const::OK;
111 }
112
113 sub fetch_user_holds {
114     my $self = shift;
115     my $hold_ids = shift;
116     my $ids_only = shift;
117     my $flesh = shift;
118     my $available = shift;
119     my $limit = shift;
120     my $offset = shift;
121
122     my $e = $self->editor;
123
124     my $circ = OpenSRF::AppSession->create('open-ils.circ');
125
126     if(!$hold_ids) {
127
128         $hold_ids = $circ->request(
129             'open-ils.circ.holds.id_list.retrieve.authoritative', 
130             $e->authtoken, 
131             $e->requestor->id
132         )->gather(1);
133     
134         $hold_ids = [ grep { defined $_ } @$hold_ids[$offset..($offset + $limit - 1)] ] if $limit or $offset;
135     }
136
137
138     return $hold_ids if $ids_only or @$hold_ids == 0;
139
140     my $args = {
141         suppress_notices => 1,
142         suppress_transits => 1,
143         suppress_mvr => 1,
144         suppress_patron_details => 1,
145         include_bre => $flesh ? 1 : 0
146     };
147
148     # ----------------------------------------------------------------
149     # Collect holds in batches of $batch_size for faster retrieval
150
151     my $batch_size = 8;
152     my $batch_idx = 0;
153     my $mk_req_batch = sub {
154         my @ses;
155         my $top_idx = $batch_idx + $batch_size;
156         while($batch_idx < $top_idx) {
157             my $hold_id = $hold_ids->[$batch_idx++];
158             last unless $hold_id;
159             my $ses = OpenSRF::AppSession->create('open-ils.circ');
160             my $req = $ses->request(
161                 'open-ils.circ.hold.details.retrieve', 
162                 $e->authtoken, $hold_id, $args);
163             push(@ses, {ses => $ses, req => $req});
164         }
165         return @ses;
166     };
167
168     my $first = 1;
169     my(@collected, @holds, @ses);
170
171     while(1) {
172         @ses = $mk_req_batch->() if $first;
173         last if $first and not @ses;
174
175         if(@collected) {
176             # If desired by the caller, filter any holds that are not available.
177             if ($available) {
178                 @collected = grep { $_->{hold}->{status} == 4 } @collected;
179             }
180             while(my $blob = pop(@collected)) {
181                 $blob->{marc_xml} = XML::LibXML->new->parse_string($blob->{hold}->{bre}->marc) if $flesh;
182                 push(@holds, $blob);
183             }
184         }
185
186         for my $req_data (@ses) {
187             push(@collected, {hold => $req_data->{req}->gather(1)});
188             $req_data->{ses}->kill_me;
189         }
190
191         @ses = $mk_req_batch->();
192         last unless @collected or @ses;
193         $first = 0;
194     }
195
196     # put the holds back into the original server sort order
197     my @sorted;
198     for my $id (@$hold_ids) {
199         push @sorted, grep { $_->{hold}->{hold}->id == $id } @holds;
200     }
201
202     return \@sorted;
203 }
204
205 sub handle_hold_update {
206     my $self = shift;
207     my $action = shift;
208     my $e = $self->editor;
209     my $url;
210
211     my @hold_ids = $self->cgi->param('hold_id'); # for non-_all actions
212     @hold_ids = @{$self->fetch_user_holds(undef, 1)} if $action =~ /_all/;
213
214     my $circ = OpenSRF::AppSession->create('open-ils.circ');
215
216     if($action =~ /cancel/) {
217
218         for my $hold_id (@hold_ids) {
219             my $resp = $circ->request(
220                 'open-ils.circ.hold.cancel', $e->authtoken, $hold_id, 6 )->gather(1); # 6 == patron-cancelled-via-opac
221         }
222
223     } elsif ($action =~ /activate|suspend/) {
224         
225         my $vlist = [];
226         for my $hold_id (@hold_ids) {
227             my $vals = {id => $hold_id};
228
229             if($action =~ /activate/) {
230                 $vals->{frozen} = 'f';
231                 $vals->{thaw_date} = undef;
232
233             } elsif($action =~ /suspend/) {
234                 $vals->{frozen} = 't';
235                 # $vals->{thaw_date} = TODO;
236             }
237             push(@$vlist, $vals);
238         }
239
240         $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
241     } elsif ($action eq 'edit') {
242
243         my @vals = map {
244             my $val = {"id" => $_};
245             $val->{"frozen"} = $self->cgi->param("frozen");
246             $val->{"pickup_lib"} = $self->cgi->param("pickup_lib");
247
248             for my $field (qw/expire_time thaw_date/) {
249                 # XXX TODO make this support other date formats, not just
250                 # MM/DD/YYYY.
251                 next unless $self->cgi->param($field) =~
252                     m:^(\d{2})/(\d{2})/(\d{4})$:;
253                 $val->{$field} = "$3-$1-$2";
254             }
255             $val;
256         } @hold_ids;
257
258         $circ->request(
259             'open-ils.circ.hold.update.batch.atomic',
260             $e->authtoken, undef, \@vals
261         )->gather(1);   # LFW XXX test for failure
262         $url = 'https://' . $self->apache->hostname . $self->ctx->{opac_root} . '/myopac/holds';
263     }
264
265     $circ->kill_me;
266     return defined($url) ? $self->generic_redirect($url) : undef;
267 }
268
269 sub load_myopac_holds {
270     my $self = shift;
271     my $e = $self->editor;
272     my $ctx = $self->ctx;
273     
274
275     my $limit = $self->cgi->param('limit') || 0;
276     my $offset = $self->cgi->param('offset') || 0;
277     my $action = $self->cgi->param('action') || '';
278     my $available = int($self->cgi->param('available') || 0);
279
280     my $hold_handle_result;
281     $hold_handle_result = $self->handle_hold_update($action) if $action;
282
283     $ctx->{holds} = $self->fetch_user_holds(undef, 0, 1, $available, $limit, $offset);
284
285     return defined($hold_handle_result) ? $hold_handle_result : Apache2::Const::OK;
286 }
287
288 sub load_place_hold {
289     my $self = shift;
290     my $ctx = $self->ctx;
291     my $e = $self->editor;
292     my $cgi = $self->cgi;
293     $self->ctx->{page} = 'place_hold';
294
295     $ctx->{hold_target} = $cgi->param('hold_target');
296     $ctx->{hold_type} = $cgi->param('hold_type');
297     $ctx->{default_pickup_lib} = $e->requestor->home_ou; # XXX staff
298
299     if ($ctx->{hold_type} eq 'T') {
300         $ctx->{record} = $e->retrieve_biblio_record_entry($ctx->{hold_target});
301     } elsif ($ctx->{hold_type} eq 'I') {
302         my $iss = $e->retrieve_serial_issuance([
303             $ctx->{hold_target}, {
304                 "flesh" => 2,
305                 "flesh_fields" => {
306                     "siss" => ["subscription"], "ssub" => ["record_entry"]
307                 }
308             }
309         ]);
310         $ctx->{record} = $iss->subscription->record_entry;
311     }
312     # ...
313
314     $ctx->{marc_xml} = XML::LibXML->new->parse_string($ctx->{record}->marc);
315
316     if(my $pickup_lib = $cgi->param('pickup_lib')) {
317
318         my $args = {
319             patronid => $e->requestor->id,
320             titleid => $ctx->{hold_target}, # XXX
321             pickup_lib => $pickup_lib,
322             depth => 0, # XXX
323         };
324
325         my $allowed = $U->simplereq(
326             'open-ils.circ',
327             'open-ils.circ.title_hold.is_possible',
328             $e->authtoken, $args
329         );
330
331         if($allowed->{success} == 1) {
332             my $hold = Fieldmapper::action::hold_request->new;
333
334             $hold->pickup_lib($pickup_lib);
335             $hold->requestor($e->requestor->id);
336             $hold->usr($e->requestor->id); # XXX staff
337             $hold->target($ctx->{hold_target});
338             $hold->hold_type($ctx->{hold_type});
339             # frozen, expired, etc..
340
341             my $stat = $U->simplereq(
342                 'open-ils.circ',
343                 'open-ils.circ.holds.create',
344                 $e->authtoken, $hold
345             );
346
347             if($stat and $stat > 0) {
348                 # if successful, return the user to the requesting page
349                 $self->apache->log->info("Redirecting back to " . $cgi->param('redirect_to'));
350                 return $self->generic_redirect;
351
352             } else {
353                 $ctx->{hold_failed} = 1;
354             }
355         } else { # hold *check* failed
356             $ctx->{hold_failed} = 1; # XXX process the events, etc
357             $ctx->{hold_failed_event} = $allowed->{last_event};
358         }
359
360         # hold permit failed
361         $logger->info('hold permit result ' . OpenSRF::Utils::JSON->perl2JSON($allowed));
362     }
363
364     return Apache2::Const::OK;
365 }
366
367
368 sub fetch_user_circs {
369     my $self = shift;
370     my $flesh = shift; # flesh bib data, etc.
371     my $circ_ids = shift;
372     my $limit = shift;
373     my $offset = shift;
374
375     my $e = $self->editor;
376
377     my @circ_ids;
378
379     if($circ_ids) {
380         @circ_ids = @$circ_ids;
381
382     } else {
383
384         my $circ_data = $U->simplereq(
385             'open-ils.actor', 
386             'open-ils.actor.user.checked_out',
387             $e->authtoken, 
388             $e->requestor->id
389         );
390
391         @circ_ids =  ( @{$circ_data->{overdue}}, @{$circ_data->{out}} );
392
393         if($limit or $offset) {
394             @circ_ids = grep { defined $_ } @circ_ids[0..($offset + $limit - 1)];
395         }
396     }
397
398     return [] unless @circ_ids;
399
400     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
401
402     my $qflesh = {
403         flesh => 3,
404         flesh_fields => {
405             circ => ['target_copy'],
406             acp => ['call_number'],
407             acn => ['record']
408         }
409     };
410
411     $e->xact_begin;
412     my $circs = $e->search_action_circulation(
413         [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
414
415     my @circs;
416     for my $circ (@$circs) {
417         push(@circs, {
418             circ => $circ, 
419             marc_xml => ($flesh and $circ->target_copy->call_number->id != -1) ? 
420                 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc) : 
421                 undef  # pre-cat copy, use the dummy title/author instead
422         });
423     }
424     $e->xact_rollback;
425
426     # make sure the final list is in the correct order
427     my @sorted_circs;
428     for my $id (@circ_ids) {
429         push(
430             @sorted_circs,
431             (grep { $_->{circ}->id == $id } @circs)
432         );
433     }
434
435     return \@sorted_circs;
436 }
437
438
439 sub handle_circ_renew {
440     my $self = shift;
441     my $action = shift;
442     my $ctx = $self->ctx;
443
444     my @renew_ids = $self->cgi->param('circ');
445
446     my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
447
448     # TODO: fire off renewal calls in batches to speed things up
449     my @responses;
450     for my $circ (@$circs) {
451
452         my $evt = $U->simplereq(
453             'open-ils.circ', 
454             'open-ils.circ.renew',
455             $self->editor->authtoken,
456             {
457                 patron_id => $self->editor->requestor->id,
458                 copy_id => $circ->{circ}->target_copy,
459                 opac_renewal => 1
460             }
461         );
462
463         # TODO return these, then insert them into the circ data 
464         # blob that is shoved into the template for each circ
465         # so the template won't have to match them
466         push(@responses, {copy => $circ->{circ}->target_copy, evt => $evt});
467     }
468
469     return @responses;
470 }
471
472
473 sub load_myopac_circs {
474     my $self = shift;
475     my $e = $self->editor;
476     my $ctx = $self->ctx;
477
478     $ctx->{circs} = [];
479     my $limit = $self->cgi->param('limit') || 0; # 0 == unlimited
480     my $offset = $self->cgi->param('offset') || 0;
481     my $action = $self->cgi->param('action') || '';
482
483     # perform the renewal first if necessary
484     my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
485
486     $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
487
488     my $success_renewals = 0;
489     my $failed_renewals = 0;
490     for my $data (@{$ctx->{circs}}) {
491         my ($resp) = grep { $_->{copy} == $data->{circ}->target_copy->id } @results;
492
493         if($resp) {
494             my $evt = ref($resp->{evt}) eq 'ARRAY' ? $resp->{evt}->[0] : $resp->{evt};
495             $data->{renewal_response} = $evt;
496             $success_renewals++ if $evt->{textcode} eq 'SUCCESS';
497             $failed_renewals++ if $evt->{textcode} ne 'SUCCESS';
498         }
499     }
500
501     $ctx->{success_renewals} = $success_renewals;
502     $ctx->{failed_renewals} = $failed_renewals;
503
504     return Apache2::Const::OK;
505 }
506
507 sub load_myopac_circ_history {
508     my $self = shift;
509     my $e = $self->editor;
510     my $ctx = $self->ctx;
511     my $limit = $self->cgi->param('limit') || 15;
512     my $offset = $self->cgi->param('offset') || 0;
513
514     $ctx->{circ_history_limit} = $limit;
515     $ctx->{circ_history_offset} = $offset;
516
517     my $circs = $e->json_query({
518         from => ['action.usr_visible_circs', $e->requestor->id],
519         #limit => $limit || 25,
520         #offset => $offset || 0,
521     });
522
523     # XXX: order-by in the json_query above appears to do nothing, so in-query 
524     # paging is not reallly an option.  do the sorting/paging here
525
526     # sort newest to oldest
527     $circs = [ sort { $b->{xact_start} cmp $a->{xact_start} } @$circs ];
528     my @ids = map { $_->{id} } @$circs;
529
530     # find the selected page and trim cruft
531     @ids = @ids[$offset..($offset + $limit - 1)] if $limit;
532     @ids = grep { defined $_ } @ids;
533
534     $ctx->{circs} = $self->fetch_user_circs(1, \@ids);
535     #$ctx->{circs} = $self->fetch_user_circs(1, [map { $_->{id} } @$circs], $limit, $offset);
536
537     return Apache2::Const::OK;
538 }
539
540 # TODO: action.usr_visible_holds does not return cancelled holds.  Should it?
541 sub load_myopac_hold_history {
542     my $self = shift;
543     my $e = $self->editor;
544     my $ctx = $self->ctx;
545     my $limit = $self->cgi->param('limit') || 15;
546     my $offset = $self->cgi->param('offset') || 0;
547     $ctx->{hold_history_limit} = $limit;
548     $ctx->{hold_history_offset} = $offset;
549
550
551     my $holds = $e->json_query({
552         from => ['action.usr_visible_holds', $e->requestor->id],
553         limit => $limit || 25,
554         offset => $offset || 0
555     });
556
557     $ctx->{holds} = $self->fetch_user_holds([map { $_->{id} } @$holds], 0, 1, 0, $limit, $offset);
558
559     return Apache2::Const::OK;
560 }
561
562 sub load_myopac_payment_form {
563     my $self = shift;
564     my $r;
565
566     $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact')]) and return $r;
567     $r = $self->prepare_extended_user_info and return $r;
568
569     return Apache2::Const::OK;
570 }
571
572 # TODO: add other filter options as params/configs/etc.
573 sub load_myopac_payments {
574     my $self = shift;
575     my $limit = $self->cgi->param('limit') || 20;
576     my $offset = $self->cgi->param('offset') || 0;
577     my $e = $self->editor;
578
579     $self->ctx->{payment_history_limit} = $limit;
580     $self->ctx->{payment_history_offset} = $offset;
581
582     my $args = {};
583     $args->{limit} = $limit if $limit;
584     $args->{offset} = $offset if $offset;
585
586     $self->ctx->{payments} = $U->simplereq(
587         'open-ils.actor',
588         'open-ils.actor.user.payments.retrieve.atomic',
589         $e->authtoken, $e->requestor->id, $args);
590
591     return Apache2::Const::OK;
592 }
593
594 sub load_myopac_pay {
595     my $self = shift;
596     my $r;
597
598     $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact')]) and
599         return $r;
600
601     # balance_owed is computed specifically from the fines we're trying
602     # to pay in this case.
603     if ($self->ctx->{fines}->{balance_owed} <= 0) {
604         $self->apache->log->info(
605             sprintf("Can't pay non-positive balance. xacts selected: (%s)",
606                 join(", ", map(int, $self->cgi->param("xact"))))
607         );
608         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
609     }
610
611     my $cc_args = {"where_process" => 1};
612
613     $cc_args->{$_} = $self->cgi->param($_) for (qw/
614         number cvv2 expire_year expire_month billing_first
615         billing_last billing_address billing_city billing_state
616         billing_zip
617     /);
618
619     my $args = {
620         "cc_args" => $cc_args,
621         "userid" => $self->ctx->{user}->id,
622         "payment_type" => "credit_card_payment",
623         "payments" => $self->prepare_fines_for_payment   # should be safe after self->prepare_fines
624     };
625
626     my $resp = $U->simplereq("open-ils.circ", "open-ils.circ.money.payment",
627         $self->editor->authtoken, $args, $self->ctx->{user}->last_xact_id
628     );
629
630     $self->ctx->{"payment_response"} = $resp;
631
632     unless ($resp->{"textcode"}) {
633         $self->ctx->{printable_receipt} = $U->simplereq(
634            "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
635            $self->editor->authtoken, $resp->{payments}
636         );
637     }
638
639     return Apache2::Const::OK;
640 }
641
642 sub load_myopac_receipt_print {
643     my $self = shift;
644
645     $self->ctx->{printable_receipt} = $U->simplereq(
646        "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
647        $self->editor->authtoken, [$self->cgi->param("payment")]
648     );
649
650     return Apache2::Const::OK;
651 }
652
653 sub load_myopac_receipt_email {
654     my $self = shift;
655
656     # The following ML method doesn't actually check whether the user in
657     # question has an email address, so we do.
658     if ($self->ctx->{user}->email) {
659         $self->ctx->{email_receipt_result} = $U->simplereq(
660            "open-ils.circ", "open-ils.circ.money.payment_receipt.email",
661            $self->editor->authtoken, [$self->cgi->param("payment")]
662         );
663     } else {
664         $self->ctx->{email_receipt_result} =
665             new OpenILS::Event("PATRON_NO_EMAIL_ADDRESS");
666     }
667
668     return Apache2::Const::OK;
669 }
670
671 sub prepare_fines {
672     my ($self, $limit, $offset, $id_list) = @_;
673
674     # XXX TODO: check for failure after various network calls
675
676     # It may be unclear, but this result structure lumps circulation and
677     # reservation fines together, and keeps grocery fines separate.
678     $self->ctx->{"fines"} = {
679         "circulation" => [],
680         "grocery" => [],
681         "total_paid" => 0,
682         "total_owed" => 0,
683         "balance_owed" => 0
684     };
685
686     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
687
688     # TODO: This should really be a ML call, but the existing calls 
689     # return an excessive amount of data and don't offer streaming
690
691     my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
692
693     my $req = $cstore->request(
694         'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
695         {
696             usr => $self->editor->requestor->id,
697             balance_owed => {'!=' => 0},
698             ($id_list && @$id_list ? ("id" => $id_list) : ()),
699         },
700         {
701             flesh => 4,
702             flesh_fields => {
703                 mobts => [qw/grocery circulation reservation/],
704                 bresv => ['target_resource_type'],
705                 brt => ['record'],
706                 mg => ['billings'],
707                 mb => ['btype'],
708                 circ => ['target_copy'],
709                 acp => ['call_number'],
710                 acn => ['record']
711             },
712             order_by => { mobts => 'xact_start' },
713             %paging
714         }
715     );
716
717     my @total_keys = qw/total_paid total_owed balance_owed/;
718     $self->ctx->{"fines"}->{@total_keys} = (0, 0, 0);
719
720     while(my $resp = $req->recv) {
721         my $mobts = $resp->content;
722         my $circ = $mobts->circulation;
723
724         my $last_billing;
725         if($mobts->grocery) {
726             my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
727             $last_billing = pop(@billings);
728         }
729
730         # XXX TODO confirm that the following, and the later division by 100.0
731         # to get a floating point representation once again, is sufficiently
732         # "money-safe" math.
733         $self->ctx->{"fines"}->{$_} += int($mobts->$_ * 100) for (@total_keys);
734
735         my $marc_xml = undef;
736         if ($mobts->xact_type eq 'reservation' and
737             $mobts->reservation->target_resource_type->record) {
738             $marc_xml = XML::LibXML->new->parse_string(
739                 $mobts->reservation->target_resource_type->record->marc
740             );
741         } elsif ($mobts->xact_type eq 'circulation' and
742             $circ->target_copy->call_number->id != -1) {
743             $marc_xml = XML::LibXML->new->parse_string(
744                 $circ->target_copy->call_number->record->marc
745             );
746         }
747
748         push(
749             @{$self->ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
750             {
751                 xact => $mobts,
752                 last_grocery_billing => $last_billing,
753                 marc_xml => $marc_xml
754             } 
755         );
756     }
757
758     $self->ctx->{"fines"}->{$_} /= 100.0 for (@total_keys);
759     return;
760 }
761
762 sub prepare_fines_for_payment {
763     # This assumes $self->prepare_fines has already been run
764     my ($self) = @_;
765
766     my @results = ();
767     if ($self->ctx->{fines}) {
768         push @results, [$_->{xact}->id, $_->{xact}->balance_owed] foreach (
769             @{$self->ctx->{fines}->{circulation}},
770             @{$self->ctx->{fines}->{grocery}}
771         );
772     }
773
774     return \@results;
775 }
776
777 sub load_myopac_main {
778     my $self = shift;
779     my $limit = $self->cgi->param('limit') || 0;
780     my $offset = $self->cgi->param('offset') || 0;
781
782     return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
783 }
784
785 sub load_myopac_update_email {
786     my $self = shift;
787     my $e = $self->editor;
788     my $ctx = $self->ctx;
789     my $email = $self->cgi->param('email') || '';
790
791     return Apache2::Const::OK 
792         unless $self->cgi->request_method eq 'POST';
793
794     unless($email =~ /.+\@.+\..+/) { # TODO better regex?
795         $ctx->{invalid_email} = $email;
796         return Apache2::Const::OK;
797     }
798
799     my $stat = $U->simplereq(
800         'open-ils.actor', 
801         'open-ils.actor.user.email.update', 
802         $e->authtoken, $email);
803
804     my $url = $self->apache->unparsed_uri;
805     $url =~ s/update_email/prefs/;
806
807     return $self->generic_redirect($url);
808 }
809
810 sub load_myopac_update_username {
811     my $self = shift;
812     my $e = $self->editor;
813     my $ctx = $self->ctx;
814     my $username = $self->cgi->param('username') || '';
815
816     return Apache2::Const::OK 
817         unless $self->cgi->request_method eq 'POST';
818
819     unless($username and $username !~ /\s/) { # any other username restrictions?
820         $ctx->{invalid_username} = $username;
821         return Apache2::Const::OK;
822     }
823
824     if($username ne $e->requestor->usrname) {
825
826         my $evt = $U->simplereq(
827             'open-ils.actor', 
828             'open-ils.actor.user.username.update', 
829             $e->authtoken, $username);
830
831         if($U->event_equals($evt, 'USERNAME_EXISTS')) {
832             $ctx->{username_exists} = $username;
833             return Apache2::Const::OK;
834         }
835     }
836
837     my $url = $self->apache->unparsed_uri;
838     $url =~ s/update_username/prefs/;
839
840     return $self->generic_redirect($url);
841 }
842
843 sub load_myopac_update_password {
844     my $self = shift;
845     my $e = $self->editor;
846     my $ctx = $self->ctx;
847
848     return Apache2::Const::OK 
849         unless $self->cgi->request_method eq 'POST';
850
851     my $current_pw = $self->cgi->param('current_pw') || '';
852     my $new_pw = $self->cgi->param('new_pw') || '';
853     my $new_pw2 = $self->cgi->param('new_pw2') || '';
854
855     unless($new_pw eq $new_pw2) {
856         $ctx->{password_nomatch} = 1;
857         return Apache2::Const::OK;
858     }
859
860     my $pw_regex = $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
861
862     if($pw_regex and $new_pw !~ /$pw_regex/) {
863         $ctx->{password_invalid} = 1;
864         return Apache2::Const::OK;
865     }
866
867     my $evt = $U->simplereq(
868         'open-ils.actor', 
869         'open-ils.actor.user.password.update', 
870         $e->authtoken, $new_pw, $current_pw);
871
872
873     if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
874         $ctx->{password_incorrect} = 1;
875         return Apache2::Const::OK;
876     }
877
878     my $url = $self->apache->unparsed_uri;
879     $url =~ s/update_password/prefs/;
880
881     return $self->generic_redirect($url);
882 }
883
884 sub load_myopac_bookbags {
885     my $self = shift;
886     my $e = $self->editor;
887     my $ctx = $self->ctx;
888
889     $e->xact_begin; # replication...
890
891     my $rv = $self->load_mylist;
892     unless($rv eq Apache2::Const::OK) {
893         $e->rollback;
894         return $rv;
895     }
896
897     my $args = {
898         order_by => {cbreb => 'name'},
899         limit => $self->cgi->param('limit') || 10,
900         offset => $self->cgi->param('offset') || 0
901     };
902
903     $ctx->{bookbags} = $e->search_container_biblio_record_entry_bucket([
904         {owner => $self->editor->requestor->id, btype => 'bookbag'},
905         # XXX what to do about the possibility of really large bookbags here?
906         {"flesh" => 1, "flesh_fields" => {"cbreb" => ["items"]}, %$args}
907     ]);
908
909     if(!$ctx->{bookbags}) {
910         $e->rollback;
911         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
912     }
913     
914     # get unique record IDs
915     my %rec_ids = ();
916     foreach my $bbag (@{$ctx->{bookbags}}) {
917         foreach my $rec_id (
918             map { $_->target_biblio_record_entry } @{$bbag->items}
919         ) {
920             $rec_ids{$rec_id} = 1;
921         }
922     }
923
924     $ctx->{bookbags_marc_xml} = $self->fetch_marc_xml_by_id([keys %rec_ids]);
925
926     $e->rollback;
927     return Apache2::Const::OK;
928 }
929
930
931 # actions are create, delete, show, hide, rename, add_rec, delete_item
932 # CGI is action, list=list_id, add_rec/record=bre_id, del_item=bucket_item_id, name=new_bucket_name
933 sub load_myopac_bookbag_update {
934     my ($self, $action, $list_id) = @_;
935     my $e = $self->editor;
936     my $cgi = $self->cgi;
937
938     $action ||= $cgi->param('action');
939     $list_id ||= $cgi->param('list');
940
941     my @add_rec = $cgi->param('add_rec') || $cgi->param('record');
942     my @del_item = $cgi->param('del_item');
943     my $shared = $cgi->param('shared');
944     my $name = $cgi->param('name');
945     my $success = 0;
946     my $list;
947
948     if($action eq 'create') {
949         $list = Fieldmapper::container::biblio_record_entry_bucket->new;
950         $list->name($name);
951         $list->owner($e->requestor->id);
952         $list->btype('bookbag');
953         $list->pub($shared ? 't' : 'f');
954         $success = $U->simplereq('open-ils.actor', 
955             'open-ils.actor.container.create', $e->authtoken, 'biblio', $list)
956
957     } else {
958
959         $list = $e->retrieve_container_biblio_record_entry_bucket($list_id);
960
961         return Apache2::Const::HTTP_BAD_REQUEST unless 
962             $list and $list->owner == $e->requestor->id;
963     }
964
965     if($action eq 'delete') {
966         $success = $U->simplereq('open-ils.actor', 
967             'open-ils.actor.container.full_delete', $e->authtoken, 'biblio', $list_id);
968
969     } elsif($action eq 'show') {
970         unless($U->is_true($list->pub)) {
971             $list->pub('t');
972             $success = $U->simplereq('open-ils.actor', 
973                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
974         }
975
976     } elsif($action eq 'hide') {
977         if($U->is_true($list->pub)) {
978             $list->pub('f');
979             $success = $U->simplereq('open-ils.actor', 
980                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
981         }
982
983     } elsif($action eq 'rename') {
984         if($name) {
985             $list->name($name);
986             $success = $U->simplereq('open-ils.actor', 
987                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
988         }
989
990     } elsif($action eq 'add_rec') {
991         foreach my $add_rec (@add_rec) {
992             my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
993             $item->bucket($list_id);
994             $item->target_biblio_record_entry($add_rec);
995             $success = $U->simplereq('open-ils.actor', 
996                 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
997             last unless $success;
998         }
999
1000     } elsif($action eq 'del_item') {
1001         foreach (@del_item) {
1002             $success = $U->simplereq(
1003                 'open-ils.actor',
1004                 'open-ils.actor.container.item.delete', $e->authtoken, 'biblio', $_
1005             );
1006             last unless $success;
1007         }
1008     }
1009
1010     return $self->generic_redirect if $success;
1011
1012     $self->ctx->{bucket_action} = $action;
1013     $self->ctx->{bucket_action_failed} = 1;
1014     return Apache2::Const::OK;
1015 }
1016
1017 1