Description: When a Merchant registers, they may claim a restaurant (Entity). They may also claim a restaurant after they have registered (for example, if they own 2 restaurants).
The verification flow is identical in both cases - we need to verify that the Merchant in fact owns this Entity.
This can be proven by (a) a phone call to Google Places number listed, (b) sending postmail to the Google Places main address listed, or (c) proof of address uploads, such as a utility bill.
Objective: Associate an Entity with a Merchant which can pay for clicks which accrue with that Entity's Offers
Complexity: Backend
Medium Frontend
Medium
Base branch: claim
Dependencies: These Requirements partially depend on Merchants - Restaurant<Detail>
Restaurant<Detail> APIs
→ .ai file can be found here: Illustrator UI files – Google Drive (or ask @Anonymous User )
The Merchant Validation flow (ie. modals) can appear in three locations:
[ 1. Post-registration ]
Right after they submit, but rather before they see this page:
The modals cannot appear before they hit Register because then there is no Merchant object to make a MerchantClaimRequest.
Therefore, a modal should pop up right above or before this "Thank you!" page encouraging the Merchant to "Verify now" if they like to.
[ 2. Waitlist page ]
If the Merchant has ownership_approved=False
, they will be shown a Waitlist page. On this page, they have the option of claiming a new restaurant, which looks exactly the same as on the Merchant Restaurant<List> page.
[ 3. Restaurant<List> page ]
On a Restaurant<List> page, a Merchant is shown their claimed restaurants plus a Search component where they can claim another restaurant.
Any request to claim a business automatically creates a MerchantClaimRequest. A MerchantClaimRequest is a many-to-many table between Merchant & Entity - that is, a claim which a Merchant makes for a given Entity.
class Entity(BaseModel):
PENDING = "PENDING"
ALREADY_CLAIMED = "ALREADY_CLAIMED"
CLAIMABLE = "CLAIMABLE"
...
merchant = models.OneToOneField('Merchant')
merchant_claim_request = models.ManyToManyField("Mercant", through="MerchantClaimRequest")
def claimable_status(self, request):
"""Returns if this Entity is claimable or not from the perspective
of a particular Merchant
Note that claimable only means the restaurant is _able to be claimed_ -
that is, a MerchantClaimRequest can be submitted by this Merchant.
"""
if self.merchant:
return ALREADY_CLAIMED
else:
if request.user.merchant_admin == self.merchant_admin_request.merchant_admin:
# If we have issued an outstanding claim already
return PENDING
else:
# Even if another Merchant has an outstanding request, it is still "claimable" from this merchant's perspective
return CLAIMABLE
class MerchantClaimRequest(BaseModel):
"""Tracks all requests by merchants to claim their Entity, using
information Google Places to verify their identity. Requests
which do not yet have `approved` data are still pending verification.
Requests which have `verdict=False` have been denied."""
PHONE = "PHONE"
POSTMAIL = "POSTMAIL"
PROOF_OF_ADDRESS = "PROOF_OF_ADDRESS"
VERIFY_METHODS = (PHONE, POSTMAIL, PROOF_OF_ADDRESS)
merchant = models.ForeignKeyField('Merchant', null=False)
entity = models.ForeignKeyField('Entity', null=False, related="merchant_claim_request)
verify_method = models.CharField(max_length=40, blank=False, choices=(VERIFY_METHODS))
verification_phrase = models.CharField(max_length=200, blank=False) # Example: "Gourmay-rabbit"
staff_comment = models.TextField(blank=True) # Example: Called owner on 2019-04-02, correctly said verification_word of "rabbit"
upload_proof = models.FileField(null=True)
approver = models.OneToOneField('User', null=True) # Example: connects to the Gourmay staff Admin account who made the verdict
approval_date = models.DatetimeField(null=True)
verdict = models.BooleanField(null=True) # Approved / Denied claim
When a Merchant is ready to start the verification process for a new restaurant (Entity), they will receive a "Would you like to verify now?" modal.
This will generate a user flow (in the UI) - as mapped in the diagram above - that ultimately creates a MerchantClaimRequest.
The UI will query two backend APIs:
/restaurants/<entity_id>/
in order to fetch the Google Places data to show to the Merchant (as well as if they previously made a verification request or not)
/restaurants/<entity_id>/verify-submit-claim/
in order to create a new MerchantClaimRequest and (in the case of verify_method=PHONE) return a verification_phrase
Once a MerchantClaimRequest is created, Gourmay Staff will contact the merchant (via the verification method specified).
Finally, once the merchant is verified (or not), the Gourmay staff user will manually set the MerchantClaimRequest verdict
to True or False.
This automatically via a trigger will remove the Merchant from the ownership_approved
waitlist (if they were or on it) AND set the Entity.merchant
FK to this Merchant (thereby establishing the relationship).
/api/restaurants/<entity_id>/
User role: Merchant
Method: GET
Testable APIs:
Claimable: http://5cd2cd68d935aa001414a08c.mockapi.io/api/restaurants-detail
Pending: http://5cd2cd68d935aa001414a08c.mockapi.io/api/restaurants-detail-pending
Claimed: http://5cd2cd68d935aa001414a08c.mockapi.io/api/restaurants-detail-claimed
Business logic:
merchant_claim_request
is resolved by checking if there is at least one NULL MerchantClaimRequest.verdict
for this (Merchant, Entity) tuple
If true, return the most recent one
Example response:
GET /restaurants/<entity_id> 200 / {
,
"claim_status": "PENDING",
"merchant_claim_request": [
{
"creation_date_utc": "2019-04-15T01:12:01.553890Z"
}
]
}
/api/restaurants/<entity_id>/submit-claim/
User role: Merchant
Method: POST
Payload
{
"verify_method": "PHONE" // also: ("POSTMAIL", "PROOF_OF_ADDRESS")
"upload_proof": "base64file" // Base64 file
}
If verify_method
== "PROOF_OF_ADDRESS", then we need to POST the File as well
Business logic:
Create a new MerchantClaimRequest
populating upload_proof
and verify_method
from the user input.
verification_phrase
should be written (and/or overwritten) by create-verification-phrase() and returned to the user.
Example response:
POST /restaurants/<entity_id>/submit-claim/ 200 / {
"verfication_phrase": "Gourmay elephant", // ONLY if verify_method=PHONE
"status": "1",
}
Only return a verification_phrase
if verify_method=PHONE
.
If it's POSTMAIL or PROOF_OF_ADDRESS, we do not want to show the code to the user. It will be mailed to them, or not required at all.
To generate a verification phrase (create-verification-phrase()), we can use the formula: "Gourmay + random animal" (GitHub link).
MerchantClaimRequests model
The Django Admin should have the following fields readable (R) and modifiable (M):
merchant_admin__name (R)entity__name (R)entity__place__data__phone (R)entity__place__data__address (R)verification_phrase (M)verify_method (M)verdict (M)
The field verification_phrase
will be auto-populated by the function create-verification-phrase() (detailed above in the Business Logic).
The field verify_method
will be populated based on user input
The field verdict
is NULL until a Gourmay staff user sets it to True or False
We should be able to delete MerchantClaimRequests in case they are erroneous or spam (ie. they are not on_delete=PROTECTED somehow).
Entity model
Merchant.ownership_approved
should be set to True when a MerchaintClaimRequest.verdict
is set to True
Entity.merchant
should be set to the verified Merchant when a MerchaintClaimRequest.verdict
is set to True
Both these fields should be visible in the Django Admin (R+W)
To Gourmay
New MerchantClaimRequest
"Reminder to verify, then approve/disapprove this user"
Nightly email: MerchantClaimRequest.objects.filter(isNull('verdict'))
"Reminder: you have <5> new merchants to verify!"
To MerchantAdmin
New MerchantClaimRequest
"We received your request to verify your business. We'll be reaching out to your shortly!"
MerchantClaimRequest approved
"All set! You can go ahead and add your first offer on this page."
MerchantClaimRequest denied
"Sorry, we couldn't verify your business. You can try again, or contact us at 555-555-5555"
Verify that a claim_status
is mutually exclusive & collectively exhaustive
Verify Merchant.ownership_approved
is True if the reverse lookup of any Entity.merchant
resolves to that Merchant
Verify Entity.claim_status
is PENDING for a given Merchant if it has a non-null outstanding MerchantClaimRequest for that entity
Verify Entity.claim_status
is ALREADY_CLAIMED for a given Merchant another Merchant has claimed that Entity
Verify Entity.claim_status
is ALREADY_CLAIMED for a given Merchant if that same Merchant has claimed the Entity
Verify Entity.claim_status
is CLAIMABLE for a given Merchant if there are no MerchantClaimRequests for a given Entity, or if all of the Entities have non-null verdicts
"True" verdict is non-null, therefore it would seem this Entity is claimable
, however that means the Entity.merchant FK is non-null, therefore it is "ALREADY CLAIMED"
@Anonymous User to check requirements with @Anonymous User & @Anonymous User
@Anonymous User to breakdown feature into Asana tasks & assign out
@Anonymous User to design Merchant Waitlist page
@Anonymous User to design Merchant "Claim a Restaurant" components
@Anonymous User to update data model
@Anonymous User to update Django Admin panel
@Anonymous User to build APIs
@Andre to implement business logic & triggers (ie. post-save logic)
@Anonymous User to create SendGrid email templates
@Anonymous User to create endpoints
@Anonymous User to build UI layouts
@Anonymous User to connect layouts to APIs
@Anonymous User to QA claim
feature