PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.18.0
GiveWP – Donation Plugin and Fundraising Platform v4.18.0
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
give / src / Donations / Repositories / DonationRepository.php

DonationRepository.php in GiveWP – Donation Plugin and Fundraising Platform 4.18.0, at src/Donations/Repositories/DonationRepository.php

712 lines 23.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\Donations\Repositories;
4
5 use Give\DonationForms\Models\DonationForm;
6 use Give\Donations\Actions\GeneratePurchaseKey;
7 use Give\Donations\Models\Donation;
8 use Give\Donations\ValueObjects\DonationMetaKeys;
9 use Give\Donations\ValueObjects\DonationMode;
10 use Give\Donations\ValueObjects\DonationStatus;
11 use Give\Framework\Database\DB;
12 use Give\Framework\Exceptions\Primitives\Exception;
13 use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
14 use Give\Framework\FieldsAPI\Field;
15 use Give\Framework\Models\ModelQueryBuilder;
16 use Give\Framework\QueryBuilder\QueryBuilder;
17 use Give\Framework\Receipts\DonationReceipt;
18 use Give\Framework\Receipts\DonationReceiptBuilder;
19 use Give\Framework\Support\Facades\DateTime\Temporal;
20 use Give\Helpers\Call;
21 use Give\Helpers\Hooks;
22 use Give\Log\Log;
23
24 /**
25 * @since 2.20.0 update amount type, fee recovered, and exchange rate
26 * @since 2.19.6
27 */
28 class DonationRepository
29 {
30 /**
31 * @var DonationNotesRepository
32 */
33 public $notes;
34
35 /**
36 * @since 2.21.0
37 */
38 public function __construct()
39 {
40 $this->notes = give(DonationNotesRepository::class);
41 }
42
43 /**
44 * @since 2.19.6
45 *
46 * @var string[]
47 */
48 private $requiredDonationProperties = [
49 'formId',
50 'status',
51 'gatewayId',
52 'amount',
53 'donorId',
54 'firstName',
55 'type',
56 'email',
57 ];
58
59 /**
60 * Get Donation By ID
61 *
62 * @since 2.19.6
63 *
64 * @return Donation|null
65 */
66 public function getById(int $donationId)
67 {
68 return $this->prepareQuery()
69 ->where('ID', $donationId)
70 ->get();
71 }
72
73 /**
74 * @since 2.21.0
75 * @return Donation|null
76 */
77 public function getByGatewayTransactionId($gatewayTransactionId)
78 {
79 return $this->queryByGatewayTransactionId($gatewayTransactionId)->get();
80 }
81
82 /**
83 * @since 3.16.0
84 */
85 public function getTotalDonationCountByGatewayTransactionId($gatewayTransactionId): int
86 {
87 return $this->queryByGatewayTransactionId($gatewayTransactionId)->count();
88 }
89
90 /**
91 * @since 2.21.0
92 */
93 public function queryByGatewayTransactionId($gatewayTransactionId): ModelQueryBuilder
94 {
95 return $this->prepareQuery()
96 ->where('post_type', 'give_payment')
97 ->where('ID', function (QueryBuilder $builder) use ($gatewayTransactionId) {
98 $builder
99 ->select('donation_id')
100 ->from('give_donationmeta')
101 ->where('meta_key', DonationMetaKeys::GATEWAY_TRANSACTION_ID()->getValue())
102 ->where('meta_value', $gatewayTransactionId);
103 });
104 }
105
106 /**
107 * @since 2.19.6
108 *
109 * @param int $donationId
110 * @return ModelQueryBuilder<Donation>
111 */
112 public function queryById(int $donationId): ModelQueryBuilder
113 {
114 return $this->prepareQuery()
115 ->where('ID', $donationId);
116 }
117
118 /**
119 * @since 2.19.6
120 *
121 * @return Donation[]|null
122 */
123 public function getBySubscriptionId(int $subscriptionId)
124 {
125 return $this->queryBySubscriptionId($subscriptionId)->getAll();
126 }
127
128 /**
129 * @since 2.19.6
130 *
131 * @param int $subscriptionId
132 * @return ModelQueryBuilder<Donation>
133 */
134 public function queryBySubscriptionId(int $subscriptionId): ModelQueryBuilder
135 {
136 $initialDonationId = give()->subscriptions->getInitialDonationId($subscriptionId);
137
138 $renewals = $this->prepareQuery()
139 ->where('post_type', 'give_payment')
140 ->where('post_status', 'give_subscription')
141 ->whereIn('ID', function (QueryBuilder $builder) use ($subscriptionId) {
142 $builder
143 ->select('donation_id')
144 ->from('give_donationmeta')
145 ->where('meta_key', DonationMetaKeys::SUBSCRIPTION_ID)
146 ->where('meta_value', $subscriptionId);
147 });
148
149 return $renewals->orWhere('ID', $initialDonationId)->orderBy('post_date', 'DESC');
150 }
151
152 /**
153 * @since 2.19.6
154 *
155 * @param int $donorId
156 * @return ModelQueryBuilder<Donation>
157 */
158 public function queryByDonorId(int $donorId): ModelQueryBuilder
159 {
160 return $this->prepareQuery()
161 ->where('post_type', 'give_payment')
162 ->whereIn('ID', function (QueryBuilder $builder) use ($donorId) {
163 $builder
164 ->select('donation_id')
165 ->from('give_donationmeta')
166 ->where('meta_key', DonationMetaKeys::DONOR_ID)
167 ->where('meta_value', $donorId);
168 })
169 ->orderBy('post_date', 'DESC');
170 }
171
172 /**
173 * @since 4.0.0
174 */
175 public function getByReceiptId(string $receiptId): ?Donation
176 {
177 return $this->queryByReceiptId($receiptId)->get();
178 }
179
180 /**
181 * @since 4.0.0
182 */
183 public function queryByReceiptId(string $receiptId): ModelQueryBuilder
184 {
185 return $this->prepareQuery()
186 ->where('post_type', 'give_payment')
187 ->whereIn('ID', function (QueryBuilder $builder) use ($receiptId) {
188 $builder
189 ->select('donation_id')
190 ->from('give_donationmeta')
191 ->where('meta_key', DonationMetaKeys::PURCHASE_KEY)
192 ->where('meta_value', $receiptId);
193 });
194 }
195
196 /**
197 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
198 * @since 4.8.1 Moved campaignId assignment logic to getCoreDonationMetaForDatabase method to eliminate code duplication.
199 * @since 3.20.0 store meta using native WP functions
200 * @since 2.23.0 retrieve the post_parent instead of relying on parentId property
201 * @since 2.21.0 replace actions with givewp_donation_creating and givewp_donation_created
202 * @since 2.20.0 mutate model and return void
203 * @since 2.19.6
204 *
205 * @return void
206 * @throws Exception|InvalidArgumentException
207 */
208 public function insert(Donation $donation)
209 {
210 $this->validateDonation($donation);
211
212 Hooks::doAction('givewp_donation_creating', $donation);
213
214 $dateCreated = Temporal::withoutMicroseconds($donation->createdAt ?: Temporal::getCurrentDateTime());
215 $dateCreatedFormatted = Temporal::getFormattedDateTime($dateCreated);
216 $dateUpdated = $donation->updatedAt ?? $dateCreated;
217 $dateUpdatedFormatted = Temporal::getFormattedDateTime($dateUpdated);
218
219 DB::query('START TRANSACTION');
220
221 try {
222 DB::table('posts')
223 ->insert([
224 'post_date' => $dateCreatedFormatted,
225 'post_date_gmt' => get_gmt_from_date($dateCreatedFormatted),
226 'post_modified' => $dateUpdatedFormatted,
227 'post_modified_gmt' => get_gmt_from_date($dateUpdatedFormatted),
228 'post_status' => $this->getPersistedDonationStatus($donation)->getValue(),
229 'post_type' => 'give_payment',
230 'post_parent' => $this->deriveLegacyDonationParentId($donation),
231 ]);
232
233 $donationId = DB::last_insert_id();
234
235 $donationMeta = $this->getCoreDonationMetaForDatabase($donation);
236
237 foreach ($donationMeta as $metaKey => $metaValue) {
238 give()->payment_meta->add_meta($donationId, $metaKey, $metaValue);
239 }
240 } catch (Exception $exception) {
241 DB::query('ROLLBACK');
242
243 Log::error('Failed creating a donation', compact('donation'));
244
245 throw new Exception('Failed creating a donation', 0, $exception);
246 }
247
248 DB::query('COMMIT');
249
250 $donation->id = $donationId;
251
252 $donation->createdAt = $dateCreated;
253 $donation->updatedAt = $dateUpdated;
254
255 if (!isset($donation->formTitle)) {
256 $donation->formTitle = $this->getFormTitle($donation->formId);
257 }
258
259 if (!isset($donation->purchaseKey)) {
260 $donation->purchaseKey = $donationMeta[DonationMetaKeys::PURCHASE_KEY];
261 }
262
263 Hooks::doAction('givewp_donation_created', $donation);
264 }
265
266 /**
267 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
268 * @since 4.6.0 Turn createdAt property updatable
269 * @since 2.23.1 Use give_update_meta() method to update entries on give_donationmeta table
270 * @since 2.23.0 retrieve the post_parent instead of relying on parentId property
271 * @since 2.21.0 replace actions with givewp_donation_updating and givewp_donation_updated
272 * @since 2.20.0 return void
273 * @since 2.19.6
274 *
275 * @return void
276 * @throws Exception|InvalidArgumentException
277 */
278 public function update(Donation $donation)
279 {
280 $this->validateDonation($donation);
281
282 Hooks::doAction('givewp_donation_updating', $donation);
283
284 $dateCreated = Temporal::withoutMicroseconds($donation->createdAt);
285 $dateCreatedFormatted = Temporal::getFormattedDateTime($dateCreated);
286 $now = Temporal::withoutMicroseconds(Temporal::getCurrentDateTime());
287 $nowFormatted = Temporal::getFormattedDateTime($now);
288
289 DB::query('START TRANSACTION');
290
291 try {
292 DB::table('posts')
293 ->where('ID', $donation->id)
294 ->update([
295 'post_date' => $dateCreatedFormatted,
296 'post_date_gmt' => get_gmt_from_date($dateCreatedFormatted),
297 'post_modified' => $nowFormatted,
298 'post_modified_gmt' => get_gmt_from_date($nowFormatted),
299 'post_status' => $this->getPersistedDonationStatus($donation)->getValue(),
300 'post_type' => 'give_payment',
301 'post_parent' => $this->deriveLegacyDonationParentId($donation),
302 ]);
303
304 foreach ($this->getCoreDonationMetaForDatabase($donation) as $metaKey => $metaValue) {
305 give()->payment_meta->update_meta($donation->id, $metaKey, $metaValue);
306 }
307 } catch (Exception $exception) {
308 DB::query('ROLLBACK');
309
310 Log::error('Failed updating a donation', compact('donation'));
311
312 throw new Exception('Failed updating a donation', 0, $exception);
313 }
314
315 $donation->updatedAt = $now;
316
317 DB::query('COMMIT');
318
319 Hooks::doAction('givewp_donation_updated', $donation);
320 }
321
322 /**
323 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
324 * @since 2.21.0 replace actions with givewp_donation_deleting and givewp_donation_deleted
325 * @since 2.20.0 consolidate meta deletion into a single query
326 * @since 2.19.6
327 *
328 * @throws Exception
329 */
330 public function delete(Donation $donation): bool
331 {
332 DB::query('START TRANSACTION');
333
334 Hooks::doAction('givewp_donation_deleting', $donation);
335
336 try {
337 DB::table('posts')
338 ->where('id', $donation->id)
339 ->delete();
340
341 DB::table('give_donationmeta')
342 ->where('donation_id', $donation->id)
343 ->delete();
344 } catch (Exception $exception) {
345 DB::query('ROLLBACK');
346
347 Log::error('Failed deleting a donation', compact('donation'));
348
349 throw new Exception('Failed deleting a donation', 0, $exception);
350 }
351
352 DB::query('COMMIT');
353
354 Hooks::doAction('givewp_donation_deleted', $donation);
355
356 return true;
357 }
358
359 /**
360 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
361 * @since 4.6.0
362 *
363 * @throws Exception
364 */
365 public function trash(Donation $donation): bool
366 {
367 DB::query('START TRANSACTION');
368
369 Hooks::doAction('givewp_donation_trashing', $donation);
370
371 try {
372 $previousStatus = DB::table('posts')
373 ->where('ID', $donation->id)
374 ->value('post_status');
375
376 give()->payment_meta->update_meta($donation->id, '_wp_trash_meta_status', $previousStatus);
377 give()->payment_meta->update_meta($donation->id, '_wp_trash_meta_time', time());
378
379 DB::table('posts')
380 ->where('ID', $donation->id)
381 ->update(['post_status' => 'trash']);
382 } catch (Exception $exception) {
383 DB::query('ROLLBACK');
384
385 Log::error('Failed trashing a donation', compact('donation'));
386
387 throw new Exception('Failed trashing a donation', 0, $exception);
388 }
389
390 DB::query('COMMIT');
391
392 Hooks::doAction('givewp_donation_trashed', $donation);
393
394 return true;
395 }
396
397 /**
398 * @since 4.18.0 Keep the original error as the previous exception when the write fails.
399 * @since 4.12.0
400 *
401 * @throws Exception
402 */
403 public function unTrash(Donation $donation): bool
404 {
405 DB::query('START TRANSACTION');
406
407 Hooks::doAction('givewp_donation_untrashing', $donation);
408
409 try {
410 $previousStatus = give()->payment_meta->get_meta($donation->id, '_wp_trash_meta_status', true);
411
412 // If no previous status was saved, default to 'publish' (completed)
413 if (empty($previousStatus)) {
414 $previousStatus = DonationStatus::COMPLETE;
415 }
416
417 DB::table('posts')
418 ->where('id', $donation->id)
419 ->update(['post_status' => $previousStatus]);
420 } catch (Exception $exception) {
421 DB::query('ROLLBACK');
422
423 Log::error('Failed untrashing a donation', compact('donation'));
424
425 throw new Exception('Failed untrashing a donation', 0, $exception);
426 }
427
428 DB::query('COMMIT');
429
430 Hooks::doAction('givewp_donation_untrashed', $donation);
431
432 return true;
433 }
434
435 /**
436 * @since 4.8.1 Consolidated campaignId assignment logic from insert method to eliminate duplication and ensure donation model is updated.
437 * @since 4.0.0 added campaignId
438 * @since 3.9.0 Added meta for phone property
439 * @since 3.2.0 added meta for honorific property
440 * @since 2.20.0 update amount to use new type, and add currency and exchange rate
441 * @since 2.19.6
442 */
443 private function getCoreDonationMetaForDatabase(Donation $donation): array
444 {
445 $meta = [
446 DonationMetaKeys::GATEWAY_TRANSACTION_ID => $donation->gatewayTransactionId,
447 DonationMetaKeys::AMOUNT => give_sanitize_amount_for_db(
448 $donation->amount->formatToDecimal(),
449 ['currency' => $donation->amount->getCurrency()]
450 ),
451 DonationMetaKeys::CURRENCY => $donation->amount->getCurrency()->getCode(),
452 DonationMetaKeys::EXCHANGE_RATE => $donation->exchangeRate,
453 DonationMetaKeys::GATEWAY => $donation->gatewayId,
454 DonationMetaKeys::DONOR_ID => $donation->donorId,
455 DonationMetaKeys::FIRST_NAME => $donation->firstName,
456 DonationMetaKeys::LAST_NAME => $donation->lastName,
457 DonationMetaKeys::EMAIL => $donation->email,
458 DonationMetaKeys::PHONE => $donation->phone,
459 DonationMetaKeys::FORM_ID => $donation->formId,
460 DonationMetaKeys::FORM_TITLE => $donation->formTitle ?? $this->getFormTitle($donation->formId),
461 DonationMetaKeys::MODE => isset($donation->mode) ?
462 $donation->mode->getValue() :
463 $this->getDefaultDonationMode()->getValue(),
464 DonationMetaKeys::PURCHASE_KEY => $donation->purchaseKey ?? Call::invoke(
465 GeneratePurchaseKey::class,
466 $donation->email
467 ),
468 DonationMetaKeys::DONOR_IP => $donation->donorIp ?? give_get_ip(),
469 DonationMetaKeys::LEVEL_ID => $donation->levelId,
470 DonationMetaKeys::ANONYMOUS => (int)$donation->anonymous,
471 DonationMetaKeys::CAMPAIGN_ID => $donation->campaignId,
472 ];
473
474 // If the donation is not associated with a campaign, try to find the campaign ID by the form ID
475 if (!$donation->campaignId) {
476 $campaign = give()->campaigns->getByFormId($donation->formId);
477 $donation->campaignId = $campaign ? $campaign->id : null;
478 }
479
480 if ($donation->campaignId !== null) {
481 $meta[DonationMetaKeys::CAMPAIGN_ID] = $donation->campaignId;
482 }
483
484 if ($donation->feeAmountRecovered !== null) {
485 $meta[DonationMetaKeys::FEE_AMOUNT_RECOVERED] = $donation->feeAmountRecovered->formatToDecimal();
486 }
487
488 if ($donation->billingAddress !== null) {
489 $meta[DonationMetaKeys::BILLING_COUNTRY] = $donation->billingAddress->country;
490 $meta[DonationMetaKeys::BILLING_ADDRESS2] = $donation->billingAddress->address2;
491 $meta[DonationMetaKeys::BILLING_CITY] = $donation->billingAddress->city;
492 $meta[DonationMetaKeys::BILLING_ADDRESS1] = $donation->billingAddress->address1;
493 $meta[DonationMetaKeys::BILLING_STATE] = $donation->billingAddress->state;
494 $meta[DonationMetaKeys::BILLING_ZIP] = $donation->billingAddress->zip;
495 }
496
497 if (isset($donation->subscriptionId)) {
498 $meta[DonationMetaKeys::SUBSCRIPTION_ID] = $donation->subscriptionId;
499 }
500
501 if ($donation->type->isSubscription()) {
502 $meta[DonationMetaKeys::SUBSCRIPTION_INITIAL_DONATION] = 1;
503 $meta[DonationMetaKeys::IS_RECURRING] = 1;
504 }
505
506 if ($donation->company !== null) {
507 $meta[DonationMetaKeys::COMPANY] = $donation->company;
508 }
509
510 if ($donation->comment !== null) {
511 $meta[DonationMetaKeys::COMMENT] = $donation->comment;
512 }
513
514 if ($donation->honorific !== null) {
515 $meta[DonationMetaKeys::HONORIFIC] = $donation->honorific;
516 }
517
518 return $meta;
519 }
520
521 /**
522 * @since 2.19.6
523 *
524 * @return int|null
525 */
526 public function getSequentialId(int $donationId)
527 {
528 $query = DB::table('give_sequential_ordering')->where('payment_id', $donationId)->get();
529
530 if (!$query) {
531 return null;
532 }
533
534 return (int)$query->id;
535 }
536
537 /**
538 * @since 2.19.6
539 *
540 * @return void
541 */
542 private function validateDonation(Donation $donation)
543 {
544 foreach ($this->requiredDonationProperties as $key) {
545 if (!isset($donation->$key)) {
546 throw new InvalidArgumentException("'$key' is required.");
547 }
548 }
549
550 if ( $donation->subscriptionId && $donation->type->isSingle()) {
551 throw new InvalidArgumentException('Subscription ID can only be set for recurring donations.');
552 }
553
554 if ( !$donation->subscriptionId && ( $donation->type->isRenewal() || $donation->type->isSubscription() ) ) {
555 throw new InvalidArgumentException('Subscription ID is required for recurring donations.');
556 }
557
558 if (!$donation->donor) {
559 throw new InvalidArgumentException("Invalid donorId, Donor does not exist");
560 }
561 }
562
563 /**
564 * Provides the donation status with consideration for the donation type. If a donation is a renewal type and its
565 * status is "complete", then the status will return "renewal". In the future, "renewal" will no longer be a valid
566 * status, at which point this function will be unnecessary.
567 *
568 * New renewal donations moving forward should set the type as "renewal" and the status as "complete".
569 *
570 * @since 2.23.0
571 */
572 private function getPersistedDonationStatus(Donation $donation): DonationStatus {
573 if ( $donation->status->isComplete() && $donation->type->isRenewal() ) {
574 return DonationStatus::RENEWAL();
575 }
576
577 return $donation->status;
578 }
579
580 /**
581 * @since 2.19.6
582 */
583 private function getDefaultDonationMode(): DonationMode
584 {
585 $mode = give_is_test_mode() ? 'test' : 'live';
586
587 return new DonationMode($mode);
588 }
589
590 /**
591 * We're moving away from using the parent_id column for donations, but we still need to support it for now as
592 * legacy code still relies on it. It is only stored and should never be used in the model.
593 *
594 * @since 2.23.0
595 */
596 private function deriveLegacyDonationParentId(Donation $donation): int
597 {
598 return $donation->type->isRenewal() ? give()->subscriptions->getInitialDonationId($donation->subscriptionId) : 0;
599 }
600
601 /**
602 * @since 2.19.6
603 */
604 public function getFormTitle(int $formId): string
605 {
606 $form = DB::table('posts')
607 ->where('id', $formId)
608 ->get();
609
610 if (!$form) {
611 return '';
612 }
613
614 return $form->post_title;
615 }
616
617 /**
618 * @since 2.23.0 no longer retrieve the post_parent from the database as parentId is deprecated
619 *
620 * @return ModelQueryBuilder<Donation>
621 */
622 public function prepareQuery(): ModelQueryBuilder
623 {
624 $builder = new ModelQueryBuilder(Donation::class);
625
626 return $builder->from('posts')
627 ->select(
628 ['ID', 'id'],
629 ['post_date', 'createdAt'],
630 ['post_modified', 'updatedAt'],
631 ['post_status', 'status']
632 )
633 ->attachMeta(
634 'give_donationmeta',
635 'ID',
636 'donation_id',
637 ...DonationMetaKeys::getColumnsForAttachMetaQuery()
638 )
639 ->where('post_type', 'give_payment');
640 }
641
642 /**
643 * @since 2.19.6
644 */
645 public function getTotalDonationCountByDonorId(int $donorId): int
646 {
647 return DB::table('posts')
648 ->where('post_type', 'give_payment')
649 ->whereIn('ID', function (QueryBuilder $builder) use ($donorId) {
650 $builder
651 ->select('donation_id')
652 ->from('give_donationmeta')
653 ->where('meta_key', DonationMetaKeys::DONOR_ID)
654 ->where('meta_value', $donorId);
655 })
656 ->count();
657 }
658
659 /**
660 * @since 2.19.6
661 *
662 * @return array|bool|null
663 */
664 public function getAllDonationIdsByDonorId(int $donorId)
665 {
666 return array_column(
667 DB::table('give_donationmeta')
668 ->select('donation_id')
669 ->where('meta_key', DonationMetaKeys::DONOR_ID)
670 ->where('meta_value', $donorId)
671 ->getAll(),
672 'donation_id'
673 );
674 }
675
676 /**
677 * @since 2.23.1 Fixed order by property, see https://github.com/impress-org/givewp/pull/6559
678 * @since 2.21.2
679 *
680 * @return Donation|null
681 */
682 public function getFirstDonation() {
683 return $this->prepareQuery()
684 ->limit(1)
685 ->orderBy('post_date', 'ASC')
686 ->get();
687 }
688
689 /**
690 * @since 2.23.1 Fixed order by property, see https://github.com/impress-org/givewp/pull/6559
691 * @since 2.21.2
692 *
693 * @return Donation|null
694 */
695 public function getLatestDonation() {
696 return $this->prepareQuery()
697 ->limit(1)
698 ->orderBy('post_date', 'DESC')
699 ->get();
700 }
701
702 /**
703 * @since 4.3.0
704 */
705 public function getConfirmationPageReceipt(Donation $donation): DonationReceipt
706 {
707 $receipt = new DonationReceipt($donation);
708
709 return (new DonationReceiptBuilder($receipt))->toConfirmationPage();
710 }
711 }
712