From 2284e4d83bb8757cf7a59077079fb6ebb1609014 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 16 Jan 2022 14:11:48 +1100 Subject: [PATCH] Refactor PostMark Webhooks --- app/Http/Controllers/PostMarkController.php | 158 +------------ app/Jobs/PostMark/ProcessPostmarkWebhook.php | 233 +++++++++++++++++++ tests/Feature/InvoiceAmountPaymentTest.php | 86 +++++++ tests/Integration/PostmarkWebhookTest.php | 141 +++++++++++ 4 files changed, 462 insertions(+), 156 deletions(-) create mode 100644 app/Jobs/PostMark/ProcessPostmarkWebhook.php create mode 100644 tests/Integration/PostmarkWebhookTest.php diff --git a/app/Http/Controllers/PostMarkController.php b/app/Http/Controllers/PostMarkController.php index 6646f1724327..ac13bb17e6d1 100644 --- a/app/Http/Controllers/PostMarkController.php +++ b/app/Http/Controllers/PostMarkController.php @@ -13,6 +13,7 @@ namespace App\Http\Controllers; use App\DataMapper\Analytics\Mail\EmailBounce; use App\DataMapper\Analytics\Mail\EmailSpam; +use App\Jobs\PostMark\ProcessPostmarkWebhook; use App\Jobs\Util\SystemLogger; use App\Libraries\MultiDB; use App\Models\CreditInvitation; @@ -77,27 +78,7 @@ class PostMarkController extends BaseController if($request->header('X-API-SECURITY') && $request->header('X-API-SECURITY') == config('postmark.secret')) { - MultiDB::findAndSetDbByCompanyKey($request->input('Tag')); - - $this->invitation = $this->discoverInvitation($request->input('MessageID')); - - if($this->invitation) - $this->invitation->email_error = $request->input('Details'); - else - return response()->json(['message' => 'Message not found']); - - switch ($request->input('RecordType')) - { - case 'Delivery': - return $this->processDelivery($request); - case 'Bounce': - return $this->processBounce($request); - case 'SpamComplaint': - return $this->processSpamComplaint($request); - default: - # code... - break; - } + ProcessPostmarkWebhook::dispatch($request->all()); return response()->json(['message' => 'Success'], 200); @@ -107,139 +88,4 @@ class PostMarkController extends BaseController } -// { -// "RecordType": "Delivery", -// "ServerID": 23, -// "MessageStream": "outbound", -// "MessageID": "00000000-0000-0000-0000-000000000000", -// "Recipient": "john@example.com", -// "Tag": "welcome-email", -// "DeliveredAt": "2021-02-21T16:34:52Z", -// "Details": "Test delivery webhook details", -// "Metadata": { -// "example": "value", -// "example_2": "value" -// } -// } - private function processDelivery($request) - { - $this->invitation->email_status = 'delivered'; - $this->invitation->save(); - - SystemLogger::dispatch($request->all(), - SystemLog::CATEGORY_MAIL, - SystemLog::EVENT_MAIL_DELIVERY, - SystemLog::TYPE_WEBHOOK_RESPONSE, - $this->invitation->contact->client, - $this->invitation->company - ); - } - -// { -// "Metadata": { -// "example": "value", -// "example_2": "value" -// }, -// "RecordType": "Bounce", -// "ID": 42, -// "Type": "HardBounce", -// "TypeCode": 1, -// "Name": "Hard bounce", -// "Tag": "Test", -// "MessageID": "00000000-0000-0000-0000-000000000000", -// "ServerID": 1234, -// "MessageStream": "outbound", -// "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).", -// "Details": "Test bounce details", -// "Email": "john@example.com", -// "From": "sender@example.com", -// "BouncedAt": "2021-02-21T16:34:52Z", -// "DumpAvailable": true, -// "Inactive": true, -// "CanActivate": true, -// "Subject": "Test subject", -// "Content": "Test content" -// } - - private function processBounce($request) - { - $this->invitation->email_status = 'bounced'; - $this->invitation->save(); - - $bounce = new EmailBounce( - $request->input('Tag'), - $request->input('From'), - $request->input('MessageID') - ); - - LightLogs::create($bounce)->queue(); - - SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); - - if(config('ninja.notification.slack')) - $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja(); - - } - -// { -// "Metadata": { -// "example": "value", -// "example_2": "value" -// }, -// "RecordType": "SpamComplaint", -// "ID": 42, -// "Type": "SpamComplaint", -// "TypeCode": 100001, -// "Name": "Spam complaint", -// "Tag": "Test", -// "MessageID": "00000000-0000-0000-0000-000000000000", -// "ServerID": 1234, -// "MessageStream": "outbound", -// "Description": "The subscriber explicitly marked this message as spam.", -// "Details": "Test spam complaint details", -// "Email": "john@example.com", -// "From": "sender@example.com", -// "BouncedAt": "2021-02-21T16:34:52Z", -// "DumpAvailable": true, -// "Inactive": true, -// "CanActivate": false, -// "Subject": "Test subject", -// "Content": "Test content" -// } - private function processSpamComplaint($request) - { - - $this->invitation->email_status = 'spam'; - $this->invitation->save(); - - $spam = new EmailSpam( - $request->input('Tag'), - $request->input('From'), - $request->input('MessageID') - ); - - LightLogs::create($spam)->queue(); - - SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); - - if(config('ninja.notification.slack')) - $this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja(); - - } - - private function discoverInvitation($message_id) - { - $invitation = false; - - if($invitation = InvoiceInvitation::where('message_id', $message_id)->first()) - return $invitation; - elseif($invitation = QuoteInvitation::where('message_id', $message_id)->first()) - return $invitation; - elseif($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first()) - return $invitation; - elseif($invitation = CreditInvitation::where('message_id', $message_id)->first()) - return $invitation; - else - return $invitation; - } } diff --git a/app/Jobs/PostMark/ProcessPostmarkWebhook.php b/app/Jobs/PostMark/ProcessPostmarkWebhook.php new file mode 100644 index 000000000000..e3444172777c --- /dev/null +++ b/app/Jobs/PostMark/ProcessPostmarkWebhook.php @@ -0,0 +1,233 @@ +request = $request; + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + + MultiDB::findAndSetDbByCompanyKey($this->request['Tag']); + + $this->invitation = $this->discoverInvitation($this->request['MessageID']); + + if(!$this->invitation) + return; + + $this->invitation->email_error = $this->request['Details']; + + switch ($this->request['RecordType']) + { + case 'Delivery': + return $this->processDelivery(); + case 'Bounce': + return $this->processBounce(); + case 'SpamComplaint': + return $this->processSpamComplaint(); + default: + # code... + break; + } + + } + +// { +// "RecordType": "Delivery", +// "ServerID": 23, +// "MessageStream": "outbound", +// "MessageID": "00000000-0000-0000-0000-000000000000", +// "Recipient": "john@example.com", +// "Tag": "welcome-email", +// "DeliveredAt": "2021-02-21T16:34:52Z", +// "Details": "Test delivery webhook details", +// "Metadata": { +// "example": "value", +// "example_2": "value" +// } +// } + private function processDelivery() + { + $this->invitation->email_status = 'delivered'; + $this->invitation->save(); + + SystemLogger::dispatch($this->request, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_DELIVERY, + SystemLog::TYPE_WEBHOOK_RESPONSE, + $this->invitation->contact->client, + $this->invitation->company + ); + } + +// { +// "Metadata": { +// "example": "value", +// "example_2": "value" +// }, +// "RecordType": "Bounce", +// "ID": 42, +// "Type": "HardBounce", +// "TypeCode": 1, +// "Name": "Hard bounce", +// "Tag": "Test", +// "MessageID": "00000000-0000-0000-0000-000000000000", +// "ServerID": 1234, +// "MessageStream": "outbound", +// "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).", +// "Details": "Test bounce details", +// "Email": "john@example.com", +// "From": "sender@example.com", +// "BouncedAt": "2021-02-21T16:34:52Z", +// "DumpAvailable": true, +// "Inactive": true, +// "CanActivate": true, +// "Subject": "Test subject", +// "Content": "Test content" +// } + + private function processBounce() + { + $this->invitation->email_status = 'bounced'; + $this->invitation->save(); + + $bounce = new EmailBounce( + $this->request['Tag'], + $this->request['From'], + $this->request['MessageID'] + ); + + LightLogs::create($bounce)->queue(); + + SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); + + if(config('ninja.notification.slack')) + $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja(); + + } + +// { +// "Metadata": { +// "example": "value", +// "example_2": "value" +// }, +// "RecordType": "SpamComplaint", +// "ID": 42, +// "Type": "SpamComplaint", +// "TypeCode": 100001, +// "Name": "Spam complaint", +// "Tag": "Test", +// "MessageID": "00000000-0000-0000-0000-000000000000", +// "ServerID": 1234, +// "MessageStream": "outbound", +// "Description": "The subscriber explicitly marked this message as spam.", +// "Details": "Test spam complaint details", +// "Email": "john@example.com", +// "From": "sender@example.com", +// "BouncedAt": "2021-02-21T16:34:52Z", +// "DumpAvailable": true, +// "Inactive": true, +// "CanActivate": false, +// "Subject": "Test subject", +// "Content": "Test content" +// } + private function processSpamComplaint() + { + + $this->invitation->email_status = 'spam'; + $this->invitation->save(); + + $spam = new EmailSpam( + $this->request['Tag'], + $this->request['From'], + $this->request['MessageID'] + ); + + LightLogs::create($spam)->queue(); + + SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); + + if(config('ninja.notification.slack')) + $this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja(); + + } + + private function discoverInvitation($message_id) + { + $invitation = false; + + if($invitation = InvoiceInvitation::where('message_id', $message_id)->first()) + return $invitation; + elseif($invitation = QuoteInvitation::where('message_id', $message_id)->first()) + return $invitation; + elseif($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first()) + return $invitation; + elseif($invitation = CreditInvitation::where('message_id', $message_id)->first()) + return $invitation; + else + return $invitation; + } +} \ No newline at end of file diff --git a/tests/Feature/InvoiceAmountPaymentTest.php b/tests/Feature/InvoiceAmountPaymentTest.php index 0d1e429f4ed7..675c6615082b 100644 --- a/tests/Feature/InvoiceAmountPaymentTest.php +++ b/tests/Feature/InvoiceAmountPaymentTest.php @@ -111,4 +111,90 @@ class InvoiceAmountPaymentTest extends TestCase $this->assertEquals(10, $payment->amount); } + + public function testMarkPaidRemovesUnpaidGatewayFees() + { + + $data = [ + 'name' => 'A Nice Client', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/clients', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $client_hash_id = $arr['data']['id']; + $client = Client::find($this->decodePrimaryKey($client_hash_id)); + + $this->assertEquals($client->balance, 0); + $this->assertEquals($client->paid_to_date, 0); + //create new invoice. + + $line_items = []; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 10; + + $line_items[] = (array)$item; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 10; + + $line_items[] = (array)$item; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 5; + $item->type_id = "3"; + + $line_items[] = (array)$item; + + $invoice = [ + 'status_id' => 1, + 'number' => '', + 'discount' => 0, + 'is_amount_discount' => 1, + 'po_number' => '3434343', + 'public_notes' => 'notes', + 'is_deleted' => 0, + 'custom_value1' => 0, + 'custom_value2' => 0, + 'custom_value3' => 0, + 'custom_value4' => 0, + 'client_id' => $client_hash_id, + 'line_items' => (array)$line_items, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices?mark_sent=true', $invoice) + ->assertStatus(200); + + $arr = $response->json(); + + $invoice_one_hashed_id = $arr['data']['id']; + + $invoice = Invoice::find($this->decodePrimaryKey($invoice_one_hashed_id)); + + $this->assertEquals(25, $invoice->balance); + $this->assertEquals(25, $invoice->amount); + + $invoice->service()->markPaid()->save(); + + $invoice->fresh(); + + $this->assertEquals(20, $invoice->amount); + $this->assertEquals(0, $invoice->balance); + + } + + } \ No newline at end of file diff --git a/tests/Integration/PostmarkWebhookTest.php b/tests/Integration/PostmarkWebhookTest.php new file mode 100644 index 000000000000..0dbd89d6513c --- /dev/null +++ b/tests/Integration/PostmarkWebhookTest.php @@ -0,0 +1,141 @@ +markTestSkipped('Postmark Secret Set'); + + $this->makeTestData(); + } + + public function testDeliveryReport() + { + + $invitation = $this->invoice->invitations->first(); + $invitation->message_id = '00000000-0000-0000-0000-000000000000'; + $invitation->save(); + + $data = [ + 'RecordType' => "Delivery", + 'ServerID' => "23", + 'MessageStream' => "outbound", + 'MessageID' => "00000000-0000-0000-0000-000000000000", + 'Recipient' => "john@example.com", + 'Tag' => $this->company->company_key, + 'DeliveredAt' => "2021-02-21T16:34:52Z", + 'Details' => "Test delivery webhook details", + ]; + + $response = $this->post('/api/v1/postmark_webhook', $data); + + $response->assertStatus(403); + + $response = $this->withHeaders([ + 'X-API-SECURITY' => config('postmark.secret') + ])->post('/api/v1/postmark_webhook', $data); + + $response->assertStatus(200); + + } + + public function testDeliveryJob() + { + + $invitation = $this->invoice->invitations->first(); + $invitation->message_id = '00000000-0000-0000-0000-000000000000'; + $invitation->save(); + + ProcessPostmarkWebhook::dispatchNow([ + 'RecordType' => "Delivery", + 'ServerID' => "23", + 'MessageStream' => "outbound", + 'MessageID' => "00000000-0000-0000-0000-000000000000", + 'Recipient' => "john@example.com", + 'Tag' => $this->company->company_key, + 'DeliveredAt' => "2021-02-21T16:34:52Z", + 'Details' => "Test delivery webhook details", + ]); + + $this->assertEquals('delivered', $invitation->fresh()->email_status); + + } + + public function testSpamReport() + { + + $invitation = $this->invoice->invitations->first(); + $invitation->message_id = '00000000-0000-0000-0000-000000000001'; + $invitation->save(); + + $data = [ + 'RecordType' => "SpamComplaint", + 'ServerID' => "23", + 'MessageStream' => "outbound", + 'MessageID' => "00000000-0000-0000-0000-000000000001", + 'Recipient' => "john@example.com", + 'Tag' => $this->company->company_key, + 'DeliveredAt' => "2021-02-21T16:34:52Z", + 'Details' => "Test delivery webhook details", + ]; + + $response = $this->post('/api/v1/postmark_webhook', $data); + + $response->assertStatus(403); + + $response = $this->withHeaders([ + 'X-API-SECURITY' => config('postmark.secret') + ])->post('/api/v1/postmark_webhook', $data); + + $response->assertStatus(200); + + } + + public function testSpamJob() + { + + $invitation = $this->invoice->invitations->first(); + $invitation->message_id = '00000000-0000-0000-0000-000000000001'; + $invitation->save(); + + ProcessPostmarkWebhook::dispatchNow([ + 'RecordType' => "SpamComplaint", + 'ServerID' => "23", + 'MessageStream' => "outbound", + 'MessageID' => "00000000-0000-0000-0000-000000000001", + 'From' => "john@example.com", + 'Tag' => $this->company->company_key, + 'DeliveredAt' => "2021-02-21T16:34:52Z", + 'Details' => "Test delivery webhook details", + ]); + + $this->assertEquals('spam', $invitation->fresh()->email_status); + + } + +}