I am trying to use the rest API to update a billing plan's setup fee. The plan is active, but not used at the moment. I create a plan for each client for each service because the values are different for each, and if they do not complete the payment process and come back at some future date the values could change.
So if they come back at some time I may need to update the setup_fee, which according to the API docs setup_fee can be changed via PATCH.
So here is the plan prior to the change request.
{
"id": "P-7H193472JB2565539MCC4REI",
"product_id": "PROD-3GK52832VM631252R",
"name": "Access to Gig",
"status": "ACTIVE",
"description": "Access to Gig",
"usage_type": "LICENSED",
"billing_cycles": [
{
"pricing_scheme": {
"version": 6,
"fixed_price": {
"currency_code": "USD",
"value": "9.44"
},
"create_time": "2021-04-25T20:25:47Z",
"update_time": "2021-04-25T20:25:47Z"
},
"frequency": {
"interval_unit": "MONTH",
"interval_count": 1
},
"tenure_type": "REGULAR",
"sequence": 1,
"total_cycles": 0
}
],
"payment_preferences": {
"service_type": "PREPAID",
"auto_bill_outstanding": true,
"setup_fee": {
"currency_code": "USD",
"value": "19.57"
},
"setup_fee_failure_action": "CANCEL",
"payment_failure_threshold": 1
},
"quantity_supported": false,
"create_time": "2021-04-25T19:52:49Z",
"update_time": "2021-04-25T20:25:47Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/billing/plans/P-7H193472JB2565539MCC4REI",
"rel": "self",
"method": "GET",
"encType": "application/json"
},
{
"href": "https://api.sandbox.paypal.com/v1/billing/plans/P-7H193472JB2565539MCC4REI",
"rel": "edit",
"method": "PATCH",
"encType": "application/json"
},
{
"href": "https://api.sandbox.paypal.com/v1/billing/plans/P-7H193472JB2565539MCC4REI/deactivate",
"rel": "self",
"method": "POST",
"encType": "application/json"
}
]
So now I want to update the setup_fee.
PATCH https://api.sandbox.paypal.com/v1/billing/plans/P-7H193472JB2565539MCC4REI
Authorization: Bearer <ACCESS TOKEN>
Content-Type: application/json
{
"path" : "payment_preferences/setup_fee",
"value" : {
"setup_fee" : {
"value" : "220.80",
"currency_code" : "USD"
}
},
"op" : "replace"
}
In response I get:
{
'details' => [
{
'location' => 'body',
'issue' => 'MALFORMED_REQUEST_JSON',
'field' => '/',
'description' => 'The request JSON is not well formed.'
}
],
'links' => [],
'message' => 'Request is not well-formed, syntactically incorrect, or violates schema.',
'name' => 'INVALID_REQUEST',
'debug_id' => '9444da3d14a00'
};
Any idea why this fails? I have tried to change the way the "value" entry is, as just the actual setup_fee, or as a hash with the entire setup_fee structure there.
Thanx
Turns out I had multiple problems, yes the first was I needed the path to be "/payment_preferences/setup_fee", I had forgotten the leading slash.
But also, I needed the json data to be an array of structures rather than a structure.
[
{
"op":"replace",
"path":"/payment_preferences/setup_fee",
"value":
{
"currency_code":"USD",
"value":"500.00"
}
}
]
Thank you all.
Related
With php and curl i create billing plans, subscriptions, get the paypal link and proceed to the payment. Everything works and i get the following webhook calls :
Billing plan created
Billing subscription created
Billing subscription activated
Paiement sale completed
So far so good. The problem is that i specified the subscription to a renewal of 1 DAY so i could check what happens when a new regular payment arrives... and nothing.
I don't know if the sandbox does not implement a real subscription with all the correct webhook callback of if i'm doing something wrong. I would be pretty pissed to be forece to develop that in production mode !
Any ideas ?
Any ideas how to test the recurring payment ?
Here is the data i get from paypal API when i call billing_subscription:
You can view it properly on https://jsoneditoronline.org/
{
"billing_cycles": [
{
"frequency": {
"Interval_count": 1,
"interval_unit": "DAY"
},
"pricing_scheme": {
"create_time": "2021-08-11T13:47:42Z",
"fixed_price": {
"currency_code": "EUR",
"value": "2.0"
},
"update_time": "2021-08-11T13:47:42Z",
"version": 1
},
"sequence": 1,
"tenure_type": "REGULAR",
"total_cycles": 120
}
],
"create_time": "2021-08-11T13:47:42Z",
"description": "offre Basic TEST avec un debit tous les jours",
"id": "P-8AF2870811319631YMEJ5J7Q",
"links": [
{
"encType": "application/json",
"href": "https://api.sandbox.paypal.com/v1/billing/plans/P-8AF2870811319631YMEJ5J7Q",
"method": "GET",
"rel": "self"
},
{
"encType": "application/json",
"href": "https://api.sandbox.paypal.com/v1/billing/plans/P-8AF2870811319631YMEJ5J7Q",
"method": "PATCH",
"rel": "edit"
},
{
"encType": "application/json",
"href": "https://api.sandbox.paypal.com/v1/billing/plans/P-8AF2870811319631YMEJ5J7Q/deactivate",
"method": "POST",
"rel": "self"
}
],
"name": "Offre Basic TEST JOUR PAR JOUR",
"payment_preferences": {
"auto_bill_outstanding": true,
"payment_failure_threshold": 3,
"service_type": "PREPAID",
"setup_fee": {
"currency_code": "EUR",
"value": "0.0"
},
"setup_fee_failure_action": "CONTINUE"
},
"product_id": "abonnement-test-basic",
"quantity_supported": false,
"status": "ACTIVE",
"taxes": {
"inclusive": false,
"percentage": "20.0"
},
"update_time": "2021-08-11T13:47:42Z",
"usage_type": "LICENSED"
}
Problem solved (sort-of...)
In production mode, when a recurring paiement is done on a subscription a webhook is called with the event : PAYMENT.SALE.COMPLETED
In sandbox mode, the event is either not sent, either randomly sent days later after when it's supposed to be.
I am currently trying the Orders API of PayPal using Postman, but cannot capture any payment.
For now, I could get the access token, set it to a collection variable, then created orders using (note the access token is set in the Authorization tab):
POST https://api-m.sandbox.paypal.com/v2/checkout/orders
Body:
{
"intent": "CAPTURE",
"purchase_units": [
{
"amount": {
"currency_code": "USD",
"value": "10.00"
}
}
]
}
The request was successful with response body:
{
"id": "<random-id>",
"status": "CREATED",
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/<random-id>",
"rel": "self",
"method": "GET"
},
{
"href": "https://www.sandbox.paypal.com/checkoutnow?token=<random-id>",
"rel": "approve",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/<random-id>",
"rel": "update",
"method": "PATCH"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/<random-id>/capture",
"rel": "capture",
"method": "POST"
}
]
}
Then I proceeded to rel:approve's link using a browser https://www.sandbox.paypal.com/checkoutnow?token=<random-id> and signed in with my sandbox account. It shows me the usual payment window but when I pressed the "Continue" button, it tried to redirect to the return page but instead, refreshed the page itself.
When I tryed to check the order using rel:self's link: GET https://api.sandbox.paypal.com/v2/checkout/orders/<random-id>. It correctly showed the sandbox account's shipping details (name and address), but the status remained CREATED (not APPROVED or COMPLETED):
{
"id": "<random-id>",
"intent": "CAPTURE",
"status": "CREATED",
"purchase_units": [
{
"reference_id": "default",
"amount": {
"currency_code": "USD",
"value": "10.00"
},
"payee": {
"email_address": "<payee-email>",
"merchant_id": "<payee-id>"
},
"shipping": {
"name": {
"full_name": "<payer-name>"
},
"address": {
"address_line_1": "<payer-address-1>",
"address_line_2": "<payer-address-2>",
"admin_area_2": "<payer-address-3>",
"admin_area_1": "<payer-address-4>",
"postal_code": "<payer-address-5>",
"country_code": "<payer-address-6>"
}
}
}
],
"create_time": "<time-of-post-request>",
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/<random-id>",
"rel": "self",
"method": "GET"
},
{
"href": "https://www.sandbox.paypal.com/checkoutnow?token=<random-id>",
"rel": "approve",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/<random-id>",
"rel": "update",
"method": "PATCH"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/<random-id>/capture",
"rel": "capture",
"method": "POST"
}
]
}
When I tried to capture the payment using rel:caputure's link: POST https://api.sandbox.paypal.com/v2/checkout/orders/<random-id>/capture with header Content Type: application/json and empty body, it said "payer has not approved the Order for payment", despite I getting the shipping details from the GET request before:
{
"name": "UNPROCESSABLE_ENTITY",
"details": [
{
"issue": "ORDER_NOT_APPROVED",
"description": "Payer has not yet approved the Order for payment. Please redirect the payer to the 'rel':'approve' url returned as part of the HATEOAS links within the Create Order call or provide a valid payment_source in the request."
}
],
"message": "The requested action could not be performed, semantically incorrect, or failed business validation.",
"debug_id": "6a10ea489ffce",
"links": [
{
"href": "https://developer.paypal.com/docs/api/orders/v2/#error-ORDER_NOT_APPROVED",
"rel": "information_link",
"method": "GET"
}
]
}
I have three questions:
Was I using the Orders API correctly? Did I miss some HTTP requests and/or some crucial steps?
I had the return URL set for my sandbox application, why did the payment page not redirect me but instead refreshed itself? Did I miss some setup beforehand?
Why did I fail to capture the payment like above?
P.S. After some digging I think I might be missing the authorize payment step but I have no idea how to do it. (Client-side request? Server-side request?)
I proceeded to rel:approve's link .. when I pressed the "Continue" button, it tried to redirect to the return page but instead, refreshed the page itself.
You did not specify a return_url , so there is nowhere to return to. Refreshing is all that can be done.
What you should do is not redirect to an approval URL, and integrate with no redirects. For this make two routes on your server, one for 'Create Order' and one for 'Capture Order', documented here. These routes should return only JSON data (no HTML or text). The latter one should (on success) store the payment details in your database before it does the return (particularly purchase_units[0].payments.captures[0].id, the PayPal transaction ID)
Pair those two routes with the following approval flow: https://developer.paypal.com/demo/checkout/#/pattern/server
I was also having trouble with this issue, I solved it by expanding the request body, much like #preston-phx said, with the return URL, and it looked something like this:
{
"intent": "CAPTURE",
"payer": {
"email_address": requestBody.payer_email
},
"purchase_units": [{
"amount": {
"currency_code": "USD",
"value": requestBody.amount
},
"payee": {
"email_address": requestBody.payee_email
},
"payment_instruction": {
"disbursement_mode": "INSTANT", // can be INSTANT or DELAYED
"platform_fees": [
{
"amount": {
"currency_code": "USD",
"value": calculateFeesFromAmount(requestBody.amount)
}
}
]
}
}],
"redirect_urls": {
"return_url": "https://example.com/paypalpay/order/approved",
"cancel_url": "https://example.com/paypalpay/order/cancelled"
},
"application_context": {
"brand_name": "Header for payment page",
"locale": "en-US",
"landing_page": "BILLING", // can be NO_PREFERENCE, LOGIN, BILLING
"shipping_preference": "NO_SHIPPING" // because I didn't want shipping info on the page,
"user_action": "PAY_NOW", // Button name, can be PAY_NOW or CONTINUE
"return_url": "https://example.com/paypalpay/order/approved",
"cancel_url": "https://example.com/paypalpay/order/cancelled"
}
}
This also helped me customise the payment page to an extent. I hope Paypal folks include these in the docs at the correct places, most of devs have to dig through a lot of documentation to create an extensive, usable request body.
I wrote some PowerShell functions to help me create user stories a bit faster, and this all works great, but now I am stuck figuring out how to create Tasks for a User Story/Work Item, and obviously having them be assigned to a specific Work Item.
I also can't find any documentation describing this.
I almost imagine that I need use the uri "https://dev.azure.com/$($Organisation)/$Project/_apis/wit/workitems/`$Task?api-version=5.1" but I can't see how to associate it with a work item as part of this, or after.
Can anyone help or point me at some actual documentation for this, please?
Edit: While looking for something else, I stumbled across this, but sadly that errors out for me, so it might be deprecated by now...
Edit; Thanks for the help everyone. This now works for me
This is my code in case it becomes useful for someone some day in the future:
#96116 is the parent work item, 96113 the child task
$ContentType = "application/json-patch+json"
$Token = System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($PersonalAccessToken)"))
$Header = #{Authorization = 'Basic ' + $Token;accept=$ContentType}
$uri = "https://dev.azure.com/$Organisation/$Project/_apis/wit/workitems/96113?api-version=6.1-preview.3"
$body= #'
[
{
"op": "add",
"path": "/relations/-",
"value": {
"rel": "System.LinkTypes.Hierarchy-Reverse",
"url": "https://dev.azure.com/$Organisation/$Project/_apis/wit/workitems/96113",
"attributes": {
"isLocked": false,
"name": "Parent"
}
}
}
]
'#
Invoke-RestMethod -Uri $uri -Method PATCH -Headers $Header -Body $Body -ContentType $contentType
You can follow the steps below to create a new Task, and link a specified User Story as the Parent of this new Task:
Use the endpoint "Work Items - Create" to create the new Task.
Use the endpoint "Work Items - Update" to link the specified User Story as the Parent.
Request URI
PATCH https://dev.azure.com/{organization}/{project}/_apis/wit/workitems/{id}?api-version=6.1-preview.3
Required Header
Content-Type: application/json-patch+json
Request Body
[
{
"op": "add",
"path": "/relations/-",
"value": {
"rel": "System.LinkTypes.Hierarchy-Reverse",
// This is the URL of the linked parent work item.
"url": "https://dev.azure.com/{organization}/{project}/_apis/wit/workItems/{parent work item ID}",
"attributes": {
"isLocked": false,
"name": "Parent"
}
}
}
]
I have tested this method, it can work fine as expected.
As mentioned above, adding a relation can be done after creation with a separate PATCH request, but you can also combine multiple Work Item Tracking requests in a single call. You need to POST to the batch endpoint and send an array of JsonPatchDocuments:
PATCH https://dev.azure.com/{organization}/{project}/_apis/wit/$batch?api-version=2.2
"Content-Type": "application/json"
"Accept": "application/json"
"Authorization": "Basic {PAT}"
[
{
"method": "PATCH",
// Replace $Task below with the WIT you want to create
"uri": "/{Project}/_apis/wit/workitems/$Task?api-version=2.2",
"headers": { "Content-Type": "application/json-patch+json" },
"body": [
{ "op": "add", "path": "/fields/System.Title", "value": "Customer can sign in using their Microsoft Account" },
// Indicates new work item instead of an update. Each new work item uses a unique negative number in the batch.
{ "op": "add", "path": "/id", "value": "-1" },
{
"op": "add",
"path": "/relations/-",
"value": {
"rel": "System.LinkTypes.Hierarchy-Reverse",
"url": "https://dev.azure.com/{organization}/{project}_apis/wit/workitems/{work item id to link to}"
}
}
]
}
]
With this API you can also create a tree of work items in a single call. You use the negative IDs to link workitems together and they ge translated to their real work item ids as the batch is executed.
Docsr for the batch API are here:
Work Item Tracking - Batch REST API
It looks like Microsoft has documentation for creating, deleting, and updating work items.
Your guess was close. Here's the provided example:
Request
POST https://dev.azure.com/fabrikam/{project}/_apis/wit/workitems/${type}?api-version=6.1-preview.3
Body
[
{
"op": "add",
"path": "/fields/System.Title",
"from": null,
"value": "Sample task"
}
]
Response
{
"id": 131489,
"rev": 1,
"fields": {
"System.AreaPath": "CustomProcessPrj",
"System.TeamProject": "CustomProcessPrj",
"System.IterationPath": "CustomProcessPrj",
"System.WorkItemType": "Task",
"System.State": "New",
"System.Reason": "New",
"System.CreatedDate": "2017-10-06T01:04:51.57Z",
"System.CreatedBy": {
"displayName": "Jamal Hartnett",
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/d291b0c4-a05c-4ea6-8df1-4b41d5f39eff",
"_links": {
"avatar": {
"href": "https://dev.azure.com/mseng/_apis/GraphProfile/MemberAvatars/aad.YTkzODFkODYtNTYxYS03ZDdiLWJjM2QtZDUzMjllMjM5OTAz"
}
},
"id": "d291b0c4-a05c-4ea6-8df1-4b41d5f39eff",
"uniqueName": "fabrikamfiber4#hotmail.com",
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=d291b0c4-a05c-4ea6-8df1-4b41d5f39eff",
"descriptor": "aad.YTkzODFkODYtNTYxYS03ZDdiLWJjM2QtZDUzMjllMjM5OTAz"
},
"System.ChangedDate": "2017-10-06T01:04:51.57Z",
"System.ChangedBy": {
"displayName": "Jamal Hartnett",
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/d291b0c4-a05c-4ea6-8df1-4b41d5f39eff",
"_links": {
"avatar": {
"href": "https://dev.azure.com/mseng/_apis/GraphProfile/MemberAvatars/aad.YTkzODFkODYtNTYxYS03ZDdiLWJjM2QtZDUzMjllMjM5OTAz"
}
},
"id": "d291b0c4-a05c-4ea6-8df1-4b41d5f39eff",
"uniqueName": "fabrikamfiber4#hotmail.com",
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=d291b0c4-a05c-4ea6-8df1-4b41d5f39eff",
"descriptor": "aad.YTkzODFkODYtNTYxYS03ZDdiLWJjM2QtZDUzMjllMjM5OTAz"
},
"System.Title": "Sample task",
"Microsoft.VSTS.Common.StateChangeDate": "2017-10-06T01:04:51.57Z",
"Microsoft.VSTS.Common.Priority": 2
},
"_links": {
"self": {
"href": "https://dev.azure.com/fabrikam/_apis/wit/workItems/131489"
},
"workItemUpdates": {
"href": "https://dev.azure.com/fabrikam/_apis/wit/workItems/131489/updates"
},
"workItemRevisions": {
"href": "https://dev.azure.com/fabrikam/_apis/wit/workItems/131489/revisions"
},
"workItemHistory": {
"href": "https://dev.azure.com/fabrikam/_apis/wit/workItems/131489/history"
},
"html": {
"href": "https://dev.azure.com/fabrikam/web/wi.aspx?pcguid=20cda608-32f0-4e6e-9b7c-8def7b38d15a&id=131489"
},
"workItemType": {
"href": "https://dev.azure.com/fabrikam/aaee31d9-14cf-48b9-a92b-3f1446c13f80/_apis/wit/workItemTypes/Task"
},
"fields": {
"href": "https://dev.azure.com/fabrikam/_apis/wit/fields"
}
},
"url": "https://dev.azure.com/fabrikam/_apis/wit/workItems/131489"
}
I'm trying to implement a simple app for Google Assistant.
All works fine, but now I have a problem with the "permission" helper :
https://developers.google.com/actions/assistant/helpers#helper_intents
I have an intent connected with webhook to my java application. When an user types a sentence similar to "near to me", I want to ask to him his location and then use lat/lon to perform a search.
es: Brazilian restaurant near to me
my intent "searchRestaurant" is fired
I receive the webhook request and I parse it
if I find a parameter that is connected to a sentence like "near to me", so instead to response with a "Card" or a "List" I return a JSON that represent the helper request :
{
"conversationToken": "[]",
"expectUserResponse": true,
"expectedInputs": [
{
"inputPrompt": {
"initialPrompts": [
{
"textToSpeech": "PLACEHOLDER_FOR_PERMISSION"
}
],
"noInputPrompts": []
},
"possibileIntents": [
{
"intent": "actions.intent.PERMISSION",
"inputValueData": {
"#type": "type.googleapis.com/google.actions.v2.PermissionValueSpec",
"optContext": "Posso accedere alla tua posizione?",
"permission": [
"NAME",
"DEVICE_PRECISE_LOCATION"
]
}
}
]
}
]
}
but something seems to be wrong, and I receive an error:
"{\n \"responseMetadata\": {\n \"status\": {\n \"code\": 10,\n \"message\": \"Failed to parse Dialogflow response into AppResponse because of empty speech response\",\n \"details\": [{\n \"#type\": \"type.googleapis.com/google.protobuf.Value\",\n \"value\": \"{\\"id\\":\\"1cc45c5e-c398-4ea7-98a5-408f31ce142d\\",\\"timestamp\\":\\"2018-08-02T14:45:05.752Z\\",\\"lang\\":\\"it\\",\\"result\\":{},\\"alternateResult\\":{},\\"status\\":{\\"code\\":206,\\"errorType\\":\\"partial_content\\",\\"errorDetails\\":\\"Webhook call failed. Error: Failed to parse webhook JSON response: Cannot find field: conversationToken in message google.cloud.dialogflow.v2.WebhookResponse.\\"},\\"sessionId\\":\\"1533221100163\\"}\"\n }]\n }\n }\n}"
The "conversationToken" is filled, so I don't understand the error message.
Maybe I'm trying to perform the operation in a wrong way.
So, which is the correct way to call this helper?
--> I've created a second intent "askGeolocation" that have "actions_intent_PERMISSION" as "Event", and ... if I understand correctly the documentation, should be trigger if the request for helper is correct.
How can I get this working?
UPDATE :
I find some example of the json response for ask permission and seems that it should be different from the one above that i'm using :
https://github.com/dialogflow/fulfillment-webhook-json/blob/master/responses/v2/ActionsOnGoogle/AskForPermission.json
{
"payload": {
"google": {
"expectUserResponse": true,
"systemIntent": {
"intent": "actions.intent.PERMISSION",
"data": {
"#type": "type.googleapis.com/google.actions.v2.PermissionValueSpec",
"optContext": "To deliver your order",
"permissions": [
"NAME",
"DEVICE_PRECISE_LOCATION"
]
}
}
}
}
}
so, i've implemented it and now the response seems to be good (no more error on parsing it), but i still receive an error on it validation :
UnparseableJsonResponse
API Version 2: Failed to parse JSON response string with 'INVALID_ARGUMENT' error: "permission: Cannot find field."
so, a problem still persist.
Anyone know the cause?
Thanks
After some tests i found the problem.
I was returning a wrong json repsonse with "permission" instead of "permissions":
"permission**s**": [
"NAME",
"DEVICE_PRECISE_LOCATION"
]
So the steps to ask for location are correct. I report them here as a little tutorial in order to help who is facing on it for the first time:
1) In DialogFlow, add some logic to your Intent, in order to understand when is ok to ask to user his location. In my case, i've added a "parameter" that identify sentences like "nearby" and so on.
2) When my Intent is fired i receive to my java application a request like this :
...
"queryResult": {
"queryText": "ristorante argentino qui vicino",
"action": "bycategory",
"parameters": {
"askgeolocation": "qui vicino",
"TipoRistorante": ["ristorante", "argentino"]
},
...
3) If "askgeolocation" parameter is filled, instead to return a "simple message" o other type of message, i return a json for ask the permission to geolocation :
{
"payload": {
"google": {
"expectUserResponse": true,
"systemIntent": {
"intent": "actions.intent.PERMISSION",
"data": {
"#type": "type.googleapis.com/google.actions.v2.PermissionValueSpec",
"optContext": "To deliver your order",
"permissions": [
"NAME",
"DEVICE_PRECISE_LOCATION"
]
}
}
}
}
}
4) You MUST have a second Intent that is configured with "actions_intent_PERMISSION " event :
No training phrases
No Action and params
No Responses
But with Fulfillment active :
5) Once your response arrive to Google Assistant this is the message that appear :
6) Now, if user answer "ok" you receive this json on your webhook :
{
"responseId": "32cf46cf-80d8-xxxxxxxxxxxxxxxxxxxxx",
"queryResult": {
"queryText": "actions_intent_PERMISSION",
"action": "geoposition",
"parameters": {
},
"allRequiredParamsPresent": true,
"fulfillmentMessages": [{
"text": {
"text": [""]
}
}],
"outputContexts": [{
"name": "projects/xxxxxxxxxxxxxxxxxxxxx"
}, {
"name": "projects/xxxxxxxxxxxxxxxxxxxxx",
"parameters": {
"PERMISSION": true
}
}, {
"name": "projects/xxxxxxxxxxxxxxxxxxxxx"
}, {
"name": "projects/xxxxxxxxxxxxxxxxxxxxx"
}, {
"name": "projects/xxxxxxxxxxxxxxxxxxxxx"
}, {
"name": "projects/xxxxxxxxxxxxxxxxxxxxx"
}],
"intent": {
"name": "projects/xxxxxxxxxxxxxxxxxxxxx",
"displayName": "geoposition"
},
"intentDetectionConfidence": 1.0,
"languageCode": "it"
},
"originalDetectIntentRequest": {
"source": "google",
"version": "2",
"payload": {
"isInSandbox": true,
"surface": {
"capabilities": [{
"name": "actions.capability.MEDIA_RESPONSE_AUDIO"
}, {
"name": "actions.capability.SCREEN_OUTPUT"
}, {
"name": "actions.capability.AUDIO_OUTPUT"
}, {
"name": "actions.capability.WEB_BROWSER"
}]
},
"requestType": "SIMULATOR",
"inputs": [{
"rawInputs": [{
"inputType": "KEYBOARD"
}],
"arguments": [{
"textValue": "true",
"name": "PERMISSION",
"boolValue": true
}],
"intent": "actions.intent.PERMISSION"
}],
"user": {
"lastSeen": "2018-08-03T08:55:20Z",
"permissions": ["NAME", "DEVICE_PRECISE_LOCATION"],
"profile": {
"displayName": ".... full name of the user ...",
"givenName": "... name ...",
"familyName": "... surname ..."
},
"locale": "it-IT",
"userId": "xxxxxxxxxxxxxxxxxxxxx"
},
"device": {
"location": {
"coordinates": {
"latitude": 45.xxxxxx,
"longitude": 9.xxxxxx
}
}
},
"conversation": {
"conversationId": "xxxxxxxxxxxxxxxxxxxxx",
"type": "ACTIVE",
"conversationToken": "[]"
},
"availableSurfaces": [{
"capabilities": [{
"name": "actions.capability.SCREEN_OUTPUT"
}, {
"name": "actions.capability.AUDIO_OUTPUT"
}, {
"name": "actions.capability.WEB_BROWSER"
}]
}]
}
},
"session": "projects/xxxxxxxxxxxxxxxxxxxxx"
}
that contains, name/surname and latitude/longitude. This information can be saved in your application, in order to not perform again this steps.
I hope this helps.
Davide
In your intent, you can ask for a parameter with a custom Entity. This you can do like this:
entity you can define as "near"
put all the synonyms for near for which you want to trigger location permission in this entity
do not mark this parameter as "required"
do not put any prompt
in the training phrases, add some phrases with this parameter
in your webhook, keep a check on the parameter, if present ask for permission if not continue.
add a permission event to another intent
do your post permission processing in that intent
Entity
Intent
I hope you get it.
There are samples on this topic specifically that guide you through exactly what's needed for requesting permissions in Node and Java.
Note: There are helper intents samples available in Node and Java as well.
I am developing an LTI Tool Provider in Python and have had no problems getting any aspects of LTI 1.x basic launch working or the Content Item specification. I have hit a huge brick wall though trying to use the LTI 2 registration mechanism.
Whenever I run my code on some Tool Consumers all I get is a 400 error when I post my tool proxy to the tool proxy endpoint listed in the tool consumer profile.
I have tried this on:
http://lti.tools/test/tc.php (Registers Succesfully)
Moodle 3.2 running on AWS (400 Bad Request)
Moodle 3.3 runnong on Localhost (Registers Succesfully)
Canvas (400 Bad Request) {"error":"Invalid Capabilities"}
Unfortunately nothing is giving me any idea of what is bad about the request. I am 90% certain that it is the JSON or something in the headers. I am pretty sure it is not the oAuth (Thanks to Comments below and the fact that it works in 2 of the 4 tested environments)
Supposedly Moodle gives you some output somewhere, but I have looked everywhere and haven't found it. I turned on the development mode from the Site Administrator Menu and followed several directions on how to make sure that errors are being logged in PHP and I can't find anything anywhere.
Originally I couldn't get this to work anywhere, but I was able to hook up xdebug to the local Moodle 3.3 instance and that's how I was finally able to get that working.
Here is the Tool Consumer Profile from Canvas:
{'#context':
['http://purl.imsglobal.org/ctx/lti/v2/ToolConsumerProfile'],
'#id': 'https://canvas.instructure.com/api/lti/courses/1146163/tool_consumer_profile/339b6700-e4cb-47c5-a54f-3ee0064921a9',
'#type': 'ToolConsumerProfile',
'capability_offered': ['basic-lti-launch-request',
'User.id',
'Canvas.api.domain',
'LtiLink.custom.url',
'ToolProxyBinding.custom.url',
'ToolProxy.custom.url',
'Canvas.placements.accountNavigation',
'Canvas.placements.courseNavigation',
'Canvas.placements.assignmentSelection',
'Canvas.placements.linkSelection',
'Canvas.placements.postGrades',
'User.username',
'Person.email.primary',
'vnd.Canvas.Person.email.sis',
'Person.name.given',
'Person.name.family',
'Person.name.full',
'CourseSection.sourcedId',
'Person.sourcedId',
'Membership.role',
'ToolConsumerProfile.url',
'Security.splitSecret',
'Context.id',
'ToolConsumerInstance.guid',
'CourseSection.sourcedId',
'Membership.role',
'Person.email.primary',
'Person.name.given',
'Person.name.family',
'Person.name.full',
'Person.sourcedId',
'User.id',
'User.image',
'Message.documentTarget',
'Message.locale',
'Context.id',
'vnd.Canvas.root_account.uuid'],
'guid': '339b6700-e4cb-47c5-a54f-3ee0064921a9',
'lti_version': 'LTI-2p0',
'product_instance': {'guid': '07adb3e60637ff02d9ea11c7c74f1ca921699bd7.canvas.instructure.com',
'product_info': {'product_family': {'code': 'canvas',
'vendor': {'code': 'https://instructure.com',
'timestamp': '2008-03-27T06:00:00Z',
'vendor_name': {'default_value': 'Instructure',
'key': 'vendor.name'}}},
'product_name': {'default_value': 'Canvas '
'by '
'Instructure',
'key': 'product.name'},
'product_version': 'none'},
'service_owner': {'description': {'default_value': 'Free '
'For '
'Teachers',
'key': 'service_owner.description'},
'service_owner_name': {'default_value': 'Free '
'For '
'Teachers',
'key': 'service_owner.name'}}},
'security_profile': [{'digest_algorithm': 'HMAC-SHA1',
'security_profile_name': 'lti_oauth_hash_message_security'},
{'digest_algorithm': 'HS256',
'security_profile_name': 'oauth2_access_token_ws_security'}],
'service_offered': [{'#id': 'https://canvas.instructure.com/api/lti/courses/1146163/tool_consumer_profile/339b6700-e4cb-47c5-a54f-3ee0064921a9#ToolProxy.collection',
'#type': 'RestService',
'action': ['POST'],
'endpoint': 'https://canvas.instructure.com/api/lti/courses/1146163/tool_proxy',
'format': ['application/vnd.ims.lti.v2.toolproxy+json']},
{'#id': 'https://canvas.instructure.com/api/lti/courses/1146163/tool_consumer_profile/339b6700-e4cb-47c5-a54f-3ee0064921a9#ToolProxy.item',
'#type': 'RestService',
'action': ['GET'],
'endpoint': 'https://canvas.instructure.com/api/lti/tool_proxy/{tool_proxy_guid}',
'format': ['application/vnd.ims.lti.v2.toolproxy+json']},
{'#id': 'https://canvas.instructure.com/api/lti/courses/1146163/tool_consumer_profile/339b6700-e4cb-47c5-a54f-3ee0064921a9#vnd.Canvas.authorization',
'#type': 'RestService',
'action': ['POST'],
'endpoint': 'https://canvas.instructure.com/api/lti/courses/1146163/authorize',
'format': ['application/json']},
{'#id': 'https://canvas.instructure.com/api/lti/courses/1146163/tool_consumer_profile/339b6700-e4cb-47c5-a54f-3ee0064921a9#ToolProxySettings',
'#type': 'RestService',
'action': ['GET', 'PUT'],
'endpoint': 'https://canvas.instructure.com/api/lti/tool_settings/tool_proxy/{tool_proxy_id}',
'format': ['application/vnd.ims.lti.v2.toolsettings+json',
'application/vnd.ims.lti.v2.toolsettings.simple+json']},
{'#id': 'https://canvas.instructure.com/api/lti/courses/1146163/tool_consumer_profile/339b6700-e4cb-47c5-a54f-3ee0064921a9#ToolProxyBindingSettings',
'#type': 'RestService',
'action': ['GET', 'PUT'],
'endpoint': 'https://canvas.instructure.com/api/lti/tool_settings/bindings/{binding_id}',
'format': ["application/vnd.ims.lti.v2.toolsettings+json'",
'application/vnd.ims.lti.v2.toolsettings.simple+json']},
{'#id': 'https://canvas.instructure.com/api/lti/courses/1146163/tool_consumer_profile/339b6700-e4cb-47c5-a54f-3ee0064921a9#LtiLinkSettings',
'#type': 'RestService',
'action': ['GET', 'PUT'],
'endpoint': 'https://canvas.instructure.com/api/lti/tool_settings/links/{tool_proxy_id}',
'format': ['application/vnd.ims.lti.v2.toolsettings+json',
'application/vnd.ims.lti.v2.toolsettings.simple+json']}]}
And Here is the Tool Proxy I am sending back (I tried to pull out anything that seemed optional.)
POST https://canvas.instructure.com/api/lti/courses/1146163/tool_proxy
{
"#type": "ToolProxy",
"#context": "http://purl.imsglobal.org/ctx/lti/v2/ToolProxy",
"tool_proxy_guid": "339b6700-e4cb-47c5-a54f-3ee0064921a9",
"tool_consumer_profile": "https://canvas.instructure.com/api/lti/courses/1146163/tool_consumer_profile",
"tool_profile": {
"base_url_choice": [
{
"default_base_url": "http://localhost:9090/",
"secure_base_url": "http://localhost:9090/",
"selector": {
"applies_to": [
"IconEndpoint",
"MessageHandler"
]
}
}
],
"lti_version": "LTI-2p0",
"product_instance": {
"guid": "1431963455",
"service_owner": {
"service_owner_name": {
"key": "service_owner.name",
"default_value": "EHR Tutor"
},
"description": {
"key": "service_owner.description",
"default_value": "Provider of high quality education"
},
"timestamp": "2017-05-04T05:37:35-05:00"
},
"product_info": {
"product_name": {
structure.com/api/lti/courses/1146163/tool_proxy/339b6700-e4cb-47c5-a54f-3ee0064921a9"\n}'
"key": "tool.name",
"default_value": "EHR Tutor"
},
"description": {
"key": "tool.description",
"default_value": "EHR Tutor"
},
"product_family": {
"vendor": {
"description": {
"key": "tool.vendor.description",
"default_value": "Noggin LLC"
},
"contact": {
"email": "info#ehrtutor.com"
},
"code": "ehrtutor.com",
"timestamp": "2017-05-04T05:37:35-05:00",
"website": "https://www.ehrtutor.com",
"vendor_name": {
"key": "tool.vendor.name",
"default_value": "Noggin LLC"
}
},
"code": "assessment-tool",
"#id": "https://my.ehrtutor.com"
},
"product_version": "0.0.1b",
"technical_description": {
"key": "tool.technical",
"default_value": "Support provided for LTI 2"
}
},
"support": {
"email": "support#ehrtutor.com"
},
"service_provider": {
"description": {
"key": "service_provider.description",
"default_value": "Service Host Provider"
},
"guid": "1431963455",
"support": {
"email": "support#ehrtutor.com"
},
"service_provider_name": {
"key": "service_provider.name",
"default_value": "EHR Tutor"
},
"timestamp": "2017-05-04T05:37:35-05:00"
}
},
"resource_handler": [
{
"resource_name": {
"key": "lesson.resource.name",
"default_value": "EHR Tutor App Launcher"
},
"description": {
"key": "lesson.resource.description",
"default_value": "Launch the EHR Tutor Application"
},
"message": [
{
"message_type": "basic-lti-launch-request",
"path": "lti",
"parameter": [
{
"variable": "User.id",
"name": "user_id"
},
{
"variable": "Person.name.given",
"name": "lis_person_name_given"
},
{
"variable": "Person.name.family",
"name": "lis_person_name_family"
},
{
"variable": "Person.name.full",
"name": "lis_person_name_full"
},
{
"variable": "Person.email.primary",
"name": "lis_person_contact_email_primary"
},
{
"variable": "Membership.role",
"name": "roles"
},
{
"variable": "Context.id",
"name": "context_id"
},
{
"variable": "Context.title",
"name": "context_title"
},
{
"variable": "ResourceLink.title",
"name": "resource_link_title"
},
{
"variable": "CourseSection.sourcedId",
"name": "lis_course_section_sourcedid"
}
]
}
],
"resource_type": {
"code": "lesson"
}
}
]
},
"lti_version": "LTI-2p0",
"security_contract": {
"shared_secret": "lgvupYnu5kaCFMWzLZkWhoKPbRaF89oyPGbTzaTwiYFpe3_c4xdQ2B-CW4-pAQeedzXxKf8h0J-T2O5tjxzFFA=="
},
"#id": "https://canvas.instructure.com/api/lti/courses/1146163/tool_proxy/339b6700-e4cb-47c5-a54f-3ee0064921a9"
}
In the end there were lots of issues that you need to be aware of when trying to register a toolproxy in LTI 2.0
Make sure that your content-type is
application/vnd.ims.lti.v2.toolproxy+json
Make sure that you have an oauth_body_hash in your Authorization
header
Check the spelling and case of all your JSON keys
Make sure that any parameters that you ask for in your resource
handlers are in the capability_offered list of the tool_consumer_profile
Use reg_key and not the guid in the tool_consumer_profile as the tool_proxy_guid/oauth_consumer_key. The guid is the same as the reg_key on Moodle, but the guid is a constant value in Canvas.
If you are using Python and need the oauth_body_hash using requests-oauthlib you can add force_include_body=True to the OAuth1 call to get it to happen.
sign = OAuth1(self.launch_params['reg_key'], self.launch_params['reg_password'],
signature_type=SIGNATURE_TYPE_AUTH_HEADER, force_include_body=True)