Tickets handling for others
Prerequisite: You must be familiar with the Syntax used in Tutorials, have already created an extension and be familiar with the XML portal logic.
- learning:
- Changing Datamodel & Portal for an Intermediate company to act in the name of final customers
- level:
- Intermediate
- domains:
- XML, Access rights, Portal
- min version:
- 2.7.0
Let's assume you company is an IT Service Provider company, and some of your final customers do not have a direct relationship with you, but are using an intermediate company, which is interacting with you on your iTop portal in the name of the final customer.
-
The intermediate company will report incidents on behalf of the client for specific services.
-
The intermediate company may play that role for multiple clients
-
The final customer can also report incidents or requests on their own.
-
The intermediate company should only be able to see the tickets they have created, but never the other tickets of its customer, which can contain sensitive information.
DataModel changes
Customers of an Intermediate
You must store for each Intermediate company which customers they are serving and even precisely which services they are serving for each customer. In this tutorial, we will consider that a Service bought by a given customer, might be outsourced to a single intermediate or not at all.
This will be achieved by adding a ExternalKey documenting the intermediate company, for a given Service delivered to a given Customer on the class Link Customer Contract / Service
- itop-design | classes
-
<class id="lnkCustomerContractToService"> <fields> <field id="managed_by_id" xsi:type="AttributeExternalKey" _delta="define"> <sql>managed_by_id</sql> <target_class>Organization</target_class> <filter/> <dependencies/> <is_null_allowed>true</is_null_allowed> <on_target_delete>DEL_MANUAL</on_target_delete> <allow_target_creation>false</allow_target_creation> </field>
Caller of a UserRequest
As the Intermediate will create in the name of a customer, we must modify the filter on Ticket class for caller_id as it may be
-
anyone from the customer organization
-
or anyone from an organization managing a service of the customer
- itop-design | classes
-
<class id="Ticket"> <fields> <field id="caller_id" xsi:type="AttributeExternalKey" _delta="must_exist"> <filter _delta="redefine"><![CDATA[ SELECT Person WHERE org_id = :this->org_id UNION SELECT Person AS p JOIN Organization AS i ON p.org_id=i.id JOIN lnkCustomerContractToService AS lnk ON lnk.managed_by_id = i.id JOIN CustomerContract AS cc ON lnk.customercontract_id = cc.id WHERE cc.org_id = :this->org_id ]]></filter> </field> <fields> </class>
You could complicate more the filter, to introduce the Service, but in that case, you would have to request the agent to select the Service before the caller, which is weird.
Proposed Services
You may want to restrict in the datamodel (so it only present valid Services based on the customer and the caller)
- itop-design | classes
-
<class id="UserRequest"> <fields> <field id="service_id" xsi:type="AttributeExternalKey" _delta="must_exist"> <filter _delta="redefine"><![CDATA[ SELECT Service AS s JOIN lnkCustomerContractToService AS l1 ON l1.service_id=s.id JOIN CustomerContract AS cc ON l1.customercontract_id=cc.id WHERE cc.org_id = :this->org_id AND s.status != 'obsolete' AND l1.managed_by_id = 0 UNION SELECT Service AS s JOIN lnkCustomerContractToService AS l1 ON l1.service_id=s.id JOIN CustomerContract AS cc ON l1.customercontract_id=cc.id JOIN Organization AS i ON l1.managed_by_id = i.id JOIN Person AS p ON p.org_id = i.id WHERE cc.org_id = :this->org_id AND s.status != 'obsolete' AND p.id = :this->caller_id ]]></filter> <dependencies> <attribute id="org_id"/> <attribute id="caller_id" _delta="define"/> </dependencies> </field> <fields> </class>
Customize the User Portal
For the Intermediate company, you want them to be able to choose the customer for which they want to create a UserRequest, before opening the Ticket creation form.
-
By the mean of a new BrowseBrick, limited to portal users having a specific profile, we can achieve this.
-
So the normal end users will still have the mosaic, but the Intermediate won't, a
list
browsing mode of their various customers would be more efficient in their case. -
Then Intermediate users will have to select the service and service subcategory within the Ticket creation form.
-
As a result, the normal portal user will also be allowed to edit the service and service subcategory within the Ticket creation form, but this is not a big deal.
The Ticket creation form in the portal has no possibility by default to choose the customer. But adding the customer field editable on the Ticket creation form is also a valid option.
New user Profile
Create a new profile with no particular access rights. Let's call it Portal Multi Contract User
- itop_design
-
<user_rights> <profiles> <profile id="912" _delta="define"> <name>Portal Multi Contract User</name> <description>Users with this profile are allowed to create UserRequest on iTop Portal in the name of customers that they serve</description> <groups/> </profile> </profiles> </user_rights>
New BrowseBrick
For this the new browseBrick must be limited to a new Portal Multi Contract User profile, here is our own example:
- itop-design | module_designs | module_design@itop_portal | bricks
-
<brick id="intermediate-customers" xsi:type="Combodo\iTop\Portal\Brick\ManageBrick" _delta="define"> <active>true</active> <width>6</width> <rank> <default>60</default> </rank> <title> <default>Brick:Portal:IntermediateCustomers:Title</default> </title> <description>Brick:Portal:IntermediateCustomers:Title+</description> <visible> <home>false</home> <navigation_menu>false</navigation_menu> </visible> <decoration_class> <default>far fa-handshake fc-3x</default> </decoration_class> <oql><![CDATA[SELECT Organization]]></oql> <export> <export_default_fields>true</export_default_fields> </export> <fields> <field id="org_id_friendlyname"/> <field id="start_date"/> <field id="end_date"/> </fields> <grouping> <tabs> <groups> <group id="all"> <rank>2</rank> <title>Brick:Portal:Licences:Tab:All</title> <condition><![CDATA[SELECT Organization]]></condition> </group> </groups> </tabs> </grouping> <security> <allowed_profiles><![CDATA[SELECT URP_Profiles WHERE name = 'Portal Multi Contract User']]></allowed_profiles> </security> </brick>
Remove access to legacy BrowseBrick
And the old BrowseBrick must be denied to the new profile
- itop-design | module_designs | module_design@itop_portal | bricks
-
<brick id="services" xsi:type="Combodo\iTop\Portal\Brick\BrowseBrick" _delta="must_exist"> <security _delta="define"> <denied_profiles><![CDATA[SELECT URP_Profiles WHERE name = 'Portal Multi Contract User']]></denied_profiles> </security> </brick>
Rework the scopes
You must rework the scopes as well,
🚧 the below examples must be reworked.
- itop-design | module_designs | module_design@itop_portal
-
<classes> <class id="Organization" _delta="must_exist"> <scopes> <scope id="intermediate" _delta="define"> <oql_view><![CDATA[SELECT Organization AS O WHERE (O.managed_by_id = :current_contact->org_id) OR (O.id = :current_contact->org_id)]]></oql_view> <allowed_profiles> <allowed_profile id="Portal Multi Contract User"/> </allowed_profiles> </scope> </scopes> </class> <class id="Contact" _delta="must_exist"> <scopes> <scope id="intermediate" _delta="define"> <oql_view><![CDATA[SELECT Contact AS C JOIN Organization AS O ON C.org_id = O.id WHERE C.status='active' AND (O.managed_by_id = :current_contact->org_id) OR (O.id = :current_contact->org_id)]]></oql_view> <oql_edit><![CDATA[SELECT Contact AS C WHERE C.org_id = :current_contact->org_id]]></oql_edit> <allowed_profiles> <allowed_profile id="Portal Multi Contract User"/> </allowed_profiles> </scope> </scopes> </class> <class id="Service" _delta="must_exist"> <scopes> <scope id="intermediate" _delta="define"> <oql_view><![CDATA[SELECT Service]]></oql_view> <allowed_profiles> <allowed_profile id="Portal Multi Contract User"/> </allowed_profiles> </scope> </scopes> </class> <class id="ServiceSubcategory" _delta="must_exist"> <scopes> <scope id="intermediate" _delta="define"> <oql_view><![CDATA[SELECT ServiceSubcategory]]></oql_view> <allowed_profiles> <allowed_profile id="Portal Multi Contract User"/> </allowed_profiles> </scope> </scopes> </class> <class id="UserRequest" _delta="must_exist"> <scopes> <scope id="portal-partner-user" _delta="define"> <oql_view><![CDATA[SELECT UserRequest AS T JOIN Organization AS O ON T.org_id = O.id WHERE (O.managed_by_id = :current_contact->org_id) OR (O.id=:current_contact->org_id)]]></oql_view> <oql_edit><![CDATA[SELECT UserRequest AS T]]></oql_edit> <allowed_profiles> <allowed_profile id="Portal Multi Contract User"/> </allowed_profiles> </scope> <scope id="in-contact" _delta="define"> <oql_view><![CDATA[SELECT UserRequest AS T JOIN lnkContactToTicket AS l ON l.ticket_id = T.id WHERE l.contact_id = :current_contact_id ]]></oql_view> <oql_edit><![CDATA[SELECT UserRequest AS T]]></oql_edit> </scope> <scope id="mine" _delta="define"> <oql_view><![CDATA[SELECT UserRequest AS T WHERE T.agent_id = :current_contact_id OR T.caller_id = :current_contact_id]]></oql_view> <oql_edit><![CDATA[SELECT UserRequest]]></oql_edit> </scope> </scopes> </class>