Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: inbound email expenses #9042

Draft
wants to merge 88 commits into
base: v5-develop
Choose a base branch
from

Conversation

paulwer
Copy link
Contributor

@paulwer paulwer commented Dec 14, 2023

this PR is a draft for importing inbound emails as expenses.
I would like to use this draft to may ask some questions and to develop this feature.

Any more providers to cover?

Status (Tests):

  • PostMark Inbound Webhook Processing
  • Mailgun Inbound Webhook Processing
  • Brevo Inbound Webhook Processing
  • InboundMailEngine
    • E-mail Format
    • Global BlackList
    • Sender Cache Blocking
    • Total Inbound Count Blocking (Cache Expiration also tested)
    • Unknown Reciepent Count Blocking (Cache Expiration also tested)
    • Company Whitelist
    • Company Blacklist
    • Allow inbound by users
    • Allow inbound by vendors
    • Allow inbound by clients
    • Allow inbound All
    • HTML Body to File
    • Expense Creation
    • Vendor Matching
    • System Logs for Blocked entities
  • Company Entity
    • Inbound Mailbox Endings
    • Inbound Mailbox Unique Systemwide Check

Dev Environment

Webhook Relay on Localhost
I have used webhook relay to simulate a internet contactable url to test the webhook behavior with each provider.

Documentation

Company Configuration

If you are starting with the IngresEngine, you should have to take a look at some major points. First of all the mailbox recieving the email has to be allowed, which can be configured at the company level. The IngresEngine can denie incomming emails, when the configuration and data related to your vendors allows it to.

  • expense_mailbox_active Enable Disable Mail Processing
  • expense_mailbox E-Mail of the recieving mailbox
  • inbound_mailbox_allow_company_users If true, all of your users of your own company are allowed to send in emails, which will not get blocked
  • inbound_mailbox_allow_vendors If true and the sender of an email matches the vendor inbound mailbox configuration it will not get blocked
  • inbound_mailbox_allow_unknown If true all mails will not get blocked
  • inbound_mailbox_whitelist a comma seperated list of additional domains or full emails, which should be allowed to send emails (even when expense_mailbox_allow_unknown is false)
  • inbound_mailbox_blacklist a comma seperated list of blocked domains or full emails, which should not be allowed to send emails (even when expense_mailbox_allow_unknown is true or the mail would be allowed by any other setting)

Vendor Matching

Incomming emails are beeing attempted to match against a vendor by checking the vendor contact emails.

SetUp Inbound Providers

SetUp Mailgun

To ensure delivery, you have to configure all used mailgun regions with a recieving configuration using store & notify.

  1. Create and setup your mailgun account
  2. Prepare your DNS settings for inbound mail processing, by setting up necessary MX records
  3. Create a recieving route using store and notify
    3a. Go to Recieving Section and click on "Create Route"
    image
    3b. Create your route by selecting your mailbox configured in invoiceninja and add notify endpoint /api/v1/mailgun_inbound_webhook
    Unbenannt

SetUp Brevo

To ensure delivery you have to configure brevo inbound parsing according to: https://developers.brevo.com/docs/inbound-parse-webhooks

  1. Create and setup your brevo account
  2. Prepare your DNS settings for inbound mail processing, by setting up necessary MX records
  3. Register the webhook via the brevo api to the following notify endpoint: /api/v1/brevo_inbound_webhook?token=brevo-api-token

SetUp Postmark

To ensure delivery you have to configure brevo inbound parsing according to: https://developers.brevo.com/docs/inbound-parse-webhooks
// TODO

SetUp with Own MailServer

If you want to use your own mailbox, you have to eigher redirect the mail to a mailbox of one of the providers above or use a provider for inbound directly.
We recommend to use one of these providers hosted on a subdomain f.ex. invoicing.xxx.com directly or to use the auto-generated mailbox of postmark when you are using redirect.

@paulwer
Copy link
Contributor Author

paulwer commented Dec 15, 2023

Edited:
Documentation:
company has gained an additional property named expense_mailbox, which is used for all webhook endpoints and can be configured with the following ENV-var:

  • INBOUND_MAILBOX_ENDINGS: comma seperated list: one of 2 options to validate a mailbox name. Checks if the mailbox ends with one of the strings. should contain the @ symbol.

@paulwer
Copy link
Contributor Author

paulwer commented Dec 16, 2023

@turbo124 please check the handling of changing the name of a mailbox and give me feedback on this.

I descided to not use a regex aproach and instead work with checks, if the provided string have a specific ending. Therefore the frontend can create a more easy combination of choosing the ending (dropdown) and let the user descide, which text he/she wants to use for the mailbox (text-input-field).
I also implemented a system-wide check that the mailbox does not exists.

app/Http/ValidationRules/Company/ValidExpenseMailbox.php

@@ -55,6 +56,8 @@ public function rules()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safe to never check the expenes mailbox in the Store Request as this is configured afterwards.... so only need a check on update

Copy link
Contributor Author

@paulwer paulwer Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@turbo124 I guess I missed this one :(
Just removing all the checks for the expense_mailbox?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@turbo124 should this not be keep in place, to suppor this, when using api create company?

config/ninja.php Outdated Show resolved Hide resolved
Copy link
Member

@turbo124 turbo124 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paulwer review complete!

Comment on lines +126 to +127
// @turbo124 TODO: how to check for services.mailgun.webhook_signing_key on company level, when custom credentials are defined
// TODO: validation for client mail credentials by recipient
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would imagine here we would need a company.settings.variable to store this?

Copy link
Contributor Author

@paulwer paulwer May 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please explain, what would you like to store here?

edit: sorry i missed my own question. I will create a setting entry for it. do you have any name suggestions or further things to considder?

return response()->json(['message' => 'Blocked.'], 403);
}

$company = MultiDB::findAndSetDbByExpenseMailbox($input["To"]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm on this one, this particular field would then also require to be unique in the system, ie company.expense_mailbox

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, thats right :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be resolved or are there any further questions on this topic?

'gocardless' => env('GOCARDLESS_KEYS', ''),
'square' => env('SQUARE_KEYS', ''),
'eway' => env('EWAY_KEYS', ''),
'paytrace' => env('PAYTRACE_KEYS', ''),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this got removed accidentally, could you add back please?

    'mollie', env('MOLLIE_KEYS',''),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh sorry, it is down below on line 102

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i saw, that you have 'mollie', env('MOLLIE_KEYS','') onstead of 'mollie' => env('MOLLIE_KEYS','') in your repo. which one is the right one?

Copy link
Member

@turbo124 turbo124 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few comments and issues to resolve!

@LarsK1
Copy link
Contributor

LarsK1 commented May 21, 2024

@paulwer I'm planning to finish the e-invoice import by end of the week. So you could integrate by calling (new ZugferdEDocument($string_file))->run(). The class then takes all the heaving processing...

@paulwer
Copy link
Contributor Author

paulwer commented May 21, 2024

@LarsK1 what happ3ns, when the document has no Zugferd Signature?
In such case the default import, like i implemented it currently should be used.

Any sugestions? Error catching or predetermining if the data required is available?

@LarsK1
Copy link
Contributor

LarsK1 commented May 21, 2024

@paulwer The easiest way, would be to first check the file ending (.pdf, .xml), then, if it's a valid ending to pass it over to the ZugferdEDocument class, which validates the file or returns an error, if the file is not valid. For the long term, we will have to create a detection library to determine, which e-invoicing standard the provided document is.

@LarsK1
Copy link
Contributor

LarsK1 commented May 21, 2024

I would raise this exception: https://github.com/horstoeko/zugferd/blob/18de152080610d6d77eda97e375bfb8ec03ef4f8/src/ZugferdDocumentReader.php#L230

@paulwer
Copy link
Contributor Author

paulwer commented May 21, 2024

@turbo124 @LarsK1 my perspective there could be cases, where an xml and a pdf is present. Does anyone of you see a good solution to determine if they are matching.

I see error cases when multiple invoices are present within one email here.

@LarsK1
Copy link
Contributor

LarsK1 commented May 21, 2024

@paulwer the first case wouldn't matter. Since the class will ignore duplicate documents (by vendor, invoice number, sum and date matching)
Regarding the second case, all system I know, do handle multiple documents per Mail. They just handle one after another...

@paulwer
Copy link
Contributor Author

paulwer commented May 21, 2024

@turbo124 @LarsK1 therefore It will stay like currently, that all docuements will be imported as distinct expense. and only those cases which can be matched by Zugferd will result in reuse.

@LarsK1 my solution does saves the email, as well as the document to the expense. Is it possible to pass the email.html document also to your sevice to enable saving this data in your expenses also. / or maybe return the expense, so I can save this document into it

Comment on lines 193 to 194
event(new ExpenseWasCreated($expense, $expense->company, Ninja::eventVars(null))); // @turbo124 please check, I copied from API-Controller
event('eloquent.created: App\Models\Expense', $expense); // @turbo124 please check, I copied from API-Controller
Copy link
Contributor Author

@paulwer paulwer May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@turbo124 when creating an expense through the api the following events are thrown:

event(new ExpenseWasCreated($expense, $expense->company, Ninja::eventVars(null))); // @turbo124 please check, I copied from API-Controller
event('eloquent.created: App\Models\Expense', $expense); // @turbo124 please check, I copied from API-Controller

should they also be present here?

@paulwer
Copy link
Contributor Author

paulwer commented May 22, 2024

@LarsK1 regarding multiple documents within one email.
What happens to cases where the sender sends additional informations like a AGB.pdf
They will currently result in an expense with no data just containing this document?

Do you think this should be wanted behavior, because of edge case? Or do you have any other idea :)

@paulwer paulwer marked this pull request as draft May 24, 2024 14:40
@LarsK1
Copy link
Contributor

LarsK1 commented May 26, 2024

@paulwer I thought about your idea regarding AGB. Most system have an "inbox" folder, where one can filter the ingested documents. Otherwise, we should have the ability to create "template" expenses, which can be deleted without any remains or accepted and then converted to a "full" expense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants